프로필

프로필 사진
Popomon
Frontend Developer
(2020/12 ~)

    카테고리

    포스트

    [Frontend/React] 19. useEffect 활용법

    2020. 10. 5. 19:55

    꿈가게: To Do List - iOS

    꿈가게: To Do List - Android

    의존성 배열 관리하기

    useEffect의 개념은 간단하지만 실전에서 사용할 때, 막히는 경우가 종종 있습니다. 특히 의존성 배열을 다루기가 까다롭습니다. 대부분은 의존성 배열을 입력하지 않아도 되지만, 종종 필요한 경우가 있습니다.

     

    의존성 배열을 잘못 관리하면  쉽게 버그로 이어지므로 가능하다면 의존성 배열을 입력하지 않는 것이 좋습니다. 몇 가지 예제를 통해서 의존성 배열을 관리하는 방법을 알아보겠습니다.

     

    여기서는 Profile 컴포넌트에서 api를 호출해서 사용자 정보를 가져오는 코드가 있습니다. useEffect 부수효과 함수는 Profile 컴포넌트가 렌더링 될 때마다 호출되기 때문에, 서버에 API를 호출하는 코드가 항상 실행이 됩니다. 이런 경우에는 빈 배열을 입력해서 마운트 될 때 한번만 호출되도록 할 수 있습니다. 하지만 이는 userId가 변경되어도 새로운 사용자 정보를 가져오지 못하기 때문에 올바른 해결책이 아닙니다. 그래서 아래와 같이 userId를 입력해 주는 것이 정확한 방법입니다.

     

    function Profile({ userId }) {
      const [user, setUser] = useState();
      useEffect(() => {
        fetchUser(userId).then(data => setUser(data));
      }, [userId]);
    }

     

    컴포넌트 안에서 사용된 상태값, 속성값 또는 이 두가지 값으로 계산된 어떤 값이 있을 때, 그것을 부수효과 함수 안에서 사용했다면, 항상 그 값은 의존성 배열에 나열을 해줘야 합니다.

     


    마운트 될 시점에만 실행되기를 원할 때

    만약, 의존성 배열에 들어갈 값이 없다면 마운트 될 시점에 한번만 실행되는 것이기 때문에 useOnMounted 라는 별도의 훅을 만들어서 사용하는 것이 훨씬 더 직관적이고 좋습니다.

     

    export default useOnMounted(effect) {
      useEffect(effect, []);
    }

     

    그리고나면 아래와 같이 적용할 수 있습니다.

     

    function Profile({ userId }) {
      const [user, setUser] = useState();
      useOnMounted(() => fetchUser(userId).then(data => setUser(data)))
    }

     


    의존성 배열을 잘못 입력한다면?

    아래의 코드를 보시면, 부수 효과 함수에서 v1, v2 두개의 상태값을 사용하고 있습니다. 하지만 작성자의 실수로 의존성배열에 v1 하나만 입력을 했습니다. 이러한 경우에 어떻게 되는지 알아보도록 하겠습니다.

     

    function MyComponent() {
      const [v1, setV1] = useState(0);
      const [v2, setV2] = useState(0);
      
      useEffect(() => {
        const id = setInterval(() => console.log(v1, v2), 1000);
        return () => clearInterval(id);
      }, [v1]);
      
      return <> ... </>
    }

     

    v1 이 변경이 되면 렌더링이 될 때, 부수효과 함수가 생성이 되고 그 함수가 나중에 비동기로 호출이 될겁니다. 그러나 이후에 v2 가 변경이 되었을 때에도 부수효과 함수는 새로생성이 되겠지만, 의존성 배열에는 v2가 없기 때문에 리액트는 방금 생성된 부수효과 함수를 무시하고 이전에 v1 이 변경될 때 생성된 부수효과 함수를 계속 사용합니다.

     

    함수가 생성될 때는 그 함수가 생성될 당시의 지역변수를 기억하고 있습니다. 그것을 실행 컨텍스트라고 합니다. v1 이 변경되었을 때 생성되는 이 함수는 그 당시의 v1, v2 값을 기억하고 있습니다. 그리고 v2가 변경이 되더라도 v1이 변경될 때 생성된 그 함수는 예전에 v2를 계속 바라보고 있는겁니다.

     

    따라서, 이렇게 의존성 배열에 v2를 입력하지 않으면, 부수효과 함수는 오래된 v2를 사용하게 되는 문제가 있습니다.

     


    부수효과 함수에 async/await 를 사용한다면?

    부수효과 함수는 항상 함수를 반환해야 합니다. 하지만 여기에 async/await 를 적용하여 아래와 같이 사용하면, 부수효과 함수가 Promise 객체를 반환하게 되기 때문에 부수효과 함수가 될 수 없습니다.

     

    useEffect(async () => {
      const data = await fetchUser(userId);
      setUser(data);
    }, [userId])

     

    따라서, async/await 를 사용하고 싶다면 함수를 별도로 정의해주어야 합니다. 하지만 여기서 문제가 있습니다. async/await 를 사용해서 작성한 이 함수는, Profile 컴포넌트가 렌더링 될 때마다 새로 생성이 됩니다. 그래서 의존성 배열의 내용이 항상 변하게 되어 부수효과 함수가 렌더링 될 때마다 실행이 됩니다.

     

    function Profile({ userId }) {
      const [user, setUser] = useState();
      async function fetchAndSetUser () {
        const data = await fetchUser(userId);
        setUser(data);
      }
    
      useEffect(() => {
        fetchAndSetUser();
      }, [fetchAndSetUser]);
      
      return (...);
    }

     

    그래서 이런 경우에는 useCallback 훅을 사용할 수 있습니다. useCallback 훅을 이용하여 userId가 변경될 때만 이 함수가 새로 생성되도록 설정합니다.

     

    function Profile({ userId }) {
      const [user, setUser] = useState();
      const fetchAndSetUser = useCallback(
        async function () {
          const data = await fetchUser(userId);
          setUser(data);
        },
        [userId]
      )
    
      useEffect(() => {
        fetchAndSetUser();
      }, [fetchAndSetUser]);
      
      return (...);
    }

     


    의존성 배열을 사용하지 않으려면?

    가능하다면 의존성 배열을 사용하지 않는 것이 좋습니다. 의존성 배열을 관리하려면 많은 시간과 노력이 들어가기 때문입니다. 특히, 속성값으로 전달되는 함수를 의존성 배열에 넣는 순간, 그 함수는 부모 컴포넌트에서 useCallback 등을 사용해서 자주 변경되지 않도록 신경써서 관리를 해야합니다.

     

     

    예제 1) API 호출 함수 - 부수효과 함수 내에서 실행 시점 조절

    의존성 배열을 입력하지 않는 대신, 아래와 같이 부수효과 함수 내에서 실행 시점을 조절할 수가 있습니다. 이렇게 함수의 실행 시점을 의존성 배열로 관리하지 않고 부수효과 함수 내에서 처리를 하면 이 부수효과 함수 안에서 사용하는 모든 변수는 최신화된 값을 참조하므로 안심할 수 있습니다.

     

    function Profile({ userId }) {
      const [user, setUser] = useState();
      async function fetchAndSetUser() {
        const data = await fetchUser(userId);
        setUser(data);
      }
    
      useEffect(() => {
        if(!user || user.id !== userId) {
          fetchAndSetUser();
        }
      });
      
      // ...
    }

     

    예제 2) 이전 상태값을 사용하는 경우 - 상태값 변경함수의 매개변수로 함수를 사용

    이전 상태값을 기반으로 다음 상태값을 계산하기 위해 상태값을 의존성 배열에 추가하는 경우가 있습니다. 

     

    function MyComponent() {
      const [count, setCount] = useState(0);
      useEffect(() => {
        function onClick() {
          setCount(count + 1);
        }
        window.addEventListener('click', onClick);
        return () => window.removeEventListener('click', onClick);
      }, [count]);
      // ...
    }

     

    이러한 경우, 의존성 배열을 사용하지 않기 위해서는 아래와 상태값 변경 함수에 함수를 입력하면 됩니다. 상태값 변경 함수에 함수를 입력 할 때에는 이 함수의 매개변수로 이전 상태값이 들어오기 때문입니다.

     

    function MyComponent() {
      const [count, setCount] = useState(0);
      useEffect(() => {
        function onClick() {
          setCount(prev => prev + 1);
        }
        window.addEventListener('click', onClick);
        return () => window.removeEventListener('click', onClick);
      });
      // ...
    }

     

    예제 3) 여러 상태값을 사용하는 경우 - useReducer 훅을 사용

    시/분/초 값을 상태값으로 관리를 하고 있고, 1초마다 한번씩 시간을 업데이트 하고 있습니다. 의존성배열에는 hour, minute, second 값을 추가를 했습니다. 이러한 여러개의 상태값을 관리하는 경우에는 어떻게 처리하는 것이 좋은지 알아보겠습니다.

     

    function Timer({ initialTotalSeconds }) {
      const [hour, setHour] = useState(Math.floor(initialTotalSeconds / 3600));
      const [minute, setMinute] = useState(Math.floor((initialTotalSeconds % 3600) / 60));
      const [second, setSecond] = useState(Math.floor(initialTotalSeconds % 60);
      
      useEffect(() => {
        const id = setInterval(() => {
          if (second) {
            setSecond(second -1);
          } else if (minute) {
            setMinute(minute -1);
            setSecond(59);
          } else if (hour) {
            setHour(hour -1);
            setMinute(59);
            setSecond(59);
          }
        }, 1000);
        return () => clearInterval(id);
      }, [hour, minute, second]);
      
      //...
    }

     

    이렇게 여러 상태값을 관리할 때에는 useReducer 훅을 사용하는 것이 좋습니다. useReducer를 사용하면 Timer 컴포넌트에서 관리해주어야할 요소는 dispatch 함수인데, dispatch 함수는 변하지 않는 요소이기 때문에 의존성 배열에서 제거할 수 있습니다.

     

    function Timer({ initialTotalSeconds }) {
      const [state, dispatch] = useReducer(reducer, {
        hour: initialTotalSeconds / 3600
        minute: (initialTotalSeconds % 3600) / 60
        second: initialTotalSeconds % 60
      });
      const { hour, minute, second } = state;
      useEffect(() => {
        const id = setInterval(dispatch, 1000);
        return () => clearInterval(id);
      });
      //...
    }
    
    function reducer(state) {
      const { hour, minute, second } = state;
      if(second) {
        return { ..state, second: second - 1 };
      } else if(second) {
        return { ..state, minute: minute - 1, second: 59 };
      } else if(second) {
        return { ..state, hour: hour - 1, minute: 59, second: 59 };
      } else {
        return state;
      }
    }

     

    예제 4) 속성값으로 전달되는 함수

    속성값으로 전달되는 함수는 내용은 그대로인데 렌더링할 때마다 변경되는 경우가 많습니다. 이로 인해서 부수효과 함수가 불필요하게 자주 호출될 수 있습니다. 이처럼 의존성 배열을 제거하기가 어려운 상황에서는 useRef 훅이 손쉬운 해결책이 될 수 있습니다.

     

    function MyComponent({ onClick }) {
      useEffect(() => {
        window.addEventListener('click', () => {
          onClick();
          // ...
        })
      }, [onClick]);
      // ...
    }

     

    렌더링이 될 때마다 ref 객체에 onClick 함수를 넣어 줍니다. 그리고 원래 사용하려던 곳에서 ref 객체를 이용하여 함수를 호출하는 방식입니다. 이렇게 부수효과 함수에서 사용된 ref 객체는 의존성 배열에 추가할 필요가 없습니다. 그런데, 의존성 배열에 이 함수를 입력하지 않으려고 너무 애쓰는 것 같아 보이긴 합니다. 실제로 불필요한 코드와 연산이 추가되기도 했습니다. 이 모든 것은 의존성 배열을 관리하는 규칙을 지키기 위한 것입니다. 이런 부분이 리액트 훅의 단점입니다.

     

    ref 객체는 컴포넌트 함수에서 직접 변경해도 된다고 생각할 수 있지만, 한가지 문제가 있습니다. 부수효과 함수에서 ref 객체를 수정하는 이유는 나중에 도입될 리액트의 concurrent mode 때문입니다. 이 모드로 동작될 때에는 컴포넌트 함수가 실행 됐다고 하더라도 중간에 렌더링이 취소될 수 있습니다. 렌더링은 취소되었는데 ref 객체에는 잘못된 값이 저장될 수 있으므로 ref 객체는 컴포넌트 함수에서 직접 수정하면 안됩니다. 단, concurrent mode로 작동하지 않는 리액트 버전이라면 문제가 되지않습니다. 나중을 위해 아래와 같이 작성하는 것이 좋습니다.

     

    function MyComponent({ onClick }) {
      const onClickRef = useRef();
      
      // concurrent mode를 위한 작성법
      useEffect(() => {
        onClickRef.current = onClick;
      });
      
      useEffect(() => {
        window.addEventListener('click', () => {
          onClickRef.current();
          // ...
        })
        // ...
      });
      // ...
    }

     

     

     

     


    출처 : 실전 리액트 프로그리맹 - 인프런 (추천합니다 !!)

     

    실전 리액트 프로그래밍 - 인프런

    [실전 리액트 프로그래밍] 책의 저자 직강! 리액트의 기초부터 실전 활용법까지 익힐 수 있습니다. 초급 프레임워크 및 라이브러리 웹 개발 Front-End React Redux 웹 개발 온라인 강의 리액트(React)를 �

    www.inflearn.com