Seung Hun

2025년 3월 20일

React Custom hook에서 좀 더 안전하게 사용하기

React로 개발을 하다 보면 Custom hook을 통해 로직을 추상화하여 재사용 합니다. 오늘은 이 Custom hook에서 memo를 사용하면 좀 더 안전하게 사용할 수 있다는 것을 알아보겠습니다.

Memo 안 하고 그냥 사용해봐요.

typescript
const usePagination = (initialPage: number) => {
  const [page, setPage] = useState(initialPage);
  const nextPage = () => setPage(page + 1);
  const prevPage = () => setPage(page - 1);

  return { page, nextPage, prevPage };
};

이런 식으로 간단한 Hook을 만들어 봤습니다.

typescript
export default function App() {
  const pagination = usePagination(0);

  return (
    <div>
      <p>{pagination.page}</p>
      <button onClick={pagination.prevPage}>이전 페이지</button>
      <button onClick={pagination.nextPage}>다음 페이지</button>
    </div>
  );
}

이렇게 사용하면

잘 동작하는 모습입니다.

문제 발생

page값으로 api를 호출해야 하는 상황이 있다고 가정해 보겠습니다.

typescript
interface Data {
  body: string;
  id: number;
  title: string;
  userId: number;
}

const getData = async ({ page }: { page: number }) => {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/posts?_page=${page}`,
  );
  const data = await response.json();
  return data;
};
typescript
export default function App() {
  const pagination = usePagination(0);

  const [data, setData] = useState<Data[]>([]);

  useEffect(() => {
    const fetchData = async () => {
      const data = await getData(pagination);
      setData(data);
    };
    fetchData();
  }, [pagination]);

  console.log(data);

  return (
    <div>
      <p>{pagination.page}</p>
      <button onClick={pagination.prevPage}>이전 페이지</button>
      <button onClick={pagination.nextPage}>다음 페이지</button>
    </div>
  );
}

이런 식으로 코드를 짤 수 있겠죠 ?

코드상으로 보면 딱히 큰 문제가 안 느껴질 수도 있을 거 같습니다.

하지만 이 코드를 실행시켜보면

무한 리렌더링이 발생합니다.

이런 현상이 발생하는 이유는 간단합니다.

  1. fetchData 실행
  2. data를 받아와서 setData 실행
  3. 리렌더링이 발생하여 pagination 객체의 참조 값 변경
  4. pagination 객체의 참조가 바뀌었기 때문에 useEffect 재실행
  5. 무한 반복

이런 이유 때문입니다.

Memo를 하면 뭐가 좋을까요 ?

물론 이 코드를

javascript
useEffect(() => {
    const fetchData = async () => {
      const data = await getData({ page: pagination.page });
      setData(data);
    };
    fetchData();
  }, [pagination.page]);

이런 식으로 pagination.page라는 원시값을 의존성으로 갖고 있으면 문제는 발생하지 않습니다.

하지만 Custom hook을 사용하는 쪽에서 어떤 실수를 할지 알 수 없기 때문에 Custom hook을 어떻게 쓰더라도 안전하게 사용할 수 있는 방법이 필요합니다.

이때 memo를 사용하면 효과적입니다.

typescript
const usePagination = (initialPage: number) => {
  const [page, setPage] = useState(initialPage);
  const nextPage = useCallback(() => setPage(page + 1), [page]);
  const prevPage = useCallback(() => setPage(page - 1), [page]);

  return useMemo(
    () => ({ page, nextPage, prevPage }),
    [nextPage, page, prevPage],
  );
};

마지막 return 할 때 useMemo를 사용하는 이유는 useCallback을 메서드에만 적용 시 hook을 사용하는 곳에서 구조 분해 할당을 사용 안 하면 위험할 수 있어서 그렇습니다.

이렇게 수정하고 아까 그 무한 렌더링을 코드를 다시 실행시켜 보면

무한 렌더링이 발생하지 않습니다.