728x90

[React] useEffect Dependency

친구와 팀 프로젝트를 진행하던 중, Hook의 Dependency 배열에 대해 질문을 받아서 논의가 이루어져 이 내용을 정리합니다.

문제가 된 코드 - tokenRefreshCallback의 dependency

const useTokenRefresher = (callback: (refresh: string) => void) => {
  React.useEffect(() => {
  try {
      const refreshCookie = Cookies.get('refresh');

      if (!refreshCookie) {
        throw 'RefreshToken does not exist.';
      }

      callback && callback(refreshCookie);
    } catch(err) {
      callback && callback('');
    }
  }, [callback]);
};

const tokenRefreshCallback = React.useCallback((refresh: string) => {
  const {dispatch} = store;

  if (refresh) {
    dispatch(tokenRefreshAction({ refresh }));
  } else {
    dispatch(clearSession());
  }
}, [store]);

useTokenRefresher(tokenRefreshCallback);

위 코드에서 tokenRefreshActionclearSession는 redux action 및 thunk입니다.

Q: dependency 배열을 빈 배열로 놔도 되지 않을까?

A: Hook을 사용할 때 dependency 배열을 생략하는 것은 좋지 않은 습관이라 생각한다. 아래 코드는 흔히 fetch할 때 작성하는 코드다.

const Search = () => {
  const [data, setData] = useState([]);

  const fetchData = async () => {
    const result = await axios('...');
    setData(result.data);
  };

  useEffect(() => {
    fetchData();
  }, []);
};

useEffect 안에 존재하는 state, prosp, 함수 등의 모든 값은 dependency 배열로 존재해야 하는데, 무한루프에 빠지는 현상 등을 막기 위해 dependency를 제거하게 될 경우, 렌더링이 무시되거나 전달해야 할 값이 갱신되지 않는 등 다른 문제를 일으킬 수 있어 좋은 해결 방법이라 생각되지 않는다. 다만, 위 코드에서는 store 전체를 필요로 하지 않고, store.dispatch만 필요로 하기 때문에 dependency 배열이 수정될 필요는 있어 보인다.

Q: 위에서 작성해주신 예시에서는 didMount 시점에서 호출하는 것이니 불필요한 dependency는 없어야 하는 게 맞는 것 같다. react-hook이 나온 이유는 component의 life cycle을 재사용할 수 있었으면 하는 부분과 business logic을 컴포넌트에서 관리하기 위함이다.아래 코드처럼 사용하는 것이 조금 더 hook 스러운 코드일 것 같은데 어떻게 생각하는가?

const handler = (callback: () => {}, deps: Array<any>) => {
  useEffect(() => {
    callback()
  }, [deps]);
};

A: react의 해당 FAQ에 적혀있는 부분이 위의 코드와 유사하다고 생각한다.

FAQ 첫 코드의 예시

function Example({ someProp }) {
  function doSomething() {
    console.log(someProp);
  }

  useEffect(() => {
    doSomething();
  }, []); // 🔴 This is not safe (it calls `doSomething` which uses `someProp`)
}

useEffect의 dependency에 doSomething 또는 someProp을 작성하라고 나와있다. 위 코드는 url을 전달받아 fetch를 DidMount 시점에 하는 코드와 비슷한 유형의 코드라고 생각한다.

useEffect(() => {
  function doSomething() {
    console.log('hello');
  }

  doSomething();
}, []); // ✅ OK in this example because we don't use *any* values from component scope

useEffect 내부에서 사용하는 외부의 값이 없다면 dependency를 []로 적는 것이 안전하다고 얘기하고 있으며, dependency를 제거하기 위해 함수를 effect 내부로 이동시키는 것을 권하고 있다. 이것이 불가능할 경우, 아래와 같은 대안이 있다고 얘기한다.

  • You can try moving that function outside of your component. In that case, the function is guaranteed to not reference any props or state, and also doesn’t need to be in the list of dependencies.
  • If the function you’re calling is a pure computation and is safe to call while rendering, you may call it outside of the effect instead, and make the effect depend on the returned value.
  • As a last resort, you can add a function to effect dependencies but wrap its definition into the useCallback Hook. This ensures it doesn’t change on every render unless its own dependencies also change

첫 코드의 경우, 가장 마지막에 기록된 dependency에 영향을 주는 함수를 useCallback으로 감싸는 방법이다. dispatch의 경우 component가 계속해서 변하지 않을 것을 보장하기 때문에 결국 callback이 변화하지 않으므로 effect 또한 한번만 실행될 것이다.

지금 다시 살펴보면 tokenRefreshCallback 함수 자체가 useEffect 내부에서만 사용되므로 이 함수를 useEffect 내부로 이동시키는 게 더 좋은 해결책이라고 생각된다. 어떻게 생각하느냐

위의 논의를 거친 끝에 수정된 코드는 아래와 같습니다.

const useTokenRefresher = (dispatch: Dispatch) => {
  React.useEffect(() => {
    const tokenRefreshCallback = (refresh: string) => {
      if (refresh) {
        dispatch(reqRefreshToken({ refresh }));
      } else {
        dispatch(clearSession());
      }
    };

    try {
      const refreshCookie = Cookies.get('refresh');

      if (!refreshCookie) {
        throw 'RefreshToken does not exist.';
      }

      tokenRefreshCallback(refreshCookie);
    } catch(err) {
      tokenRefreshCallback('');
    }
  }, []);
};

참조

728x90
728x90