Seung Hun

DALLE_2025-03-15_00.40.14_-_A_conceptual_illustration_of_Reacts_useCallback_and_useMemo_hooks.__1.useCallback-A_box_labeled_Reused_Function.-Inside_the_box_a_function.webp

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

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

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

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

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

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> ); }

이렇게 사용하면

%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2025-03-20_%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE_11.00.39.png

잘 동작하는 모습입니다.

문제 발생

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

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; };
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> ); }

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

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

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

%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2025-03-20_%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE_11.08.27.png

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

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

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

이런 이유 때문입니다.

Memo를 하면 뭐가 좋을까요 ?

물론 이 코드를

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

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

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

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

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을 사용하는 곳에서 구조 분해 할당을 사용 안 하면 위험할 수 있어서 그렇습니다.

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

%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2025-03-20_%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE_11.25.41.png

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