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> ); }
이렇게 사용하면
잘 동작하는 모습입니다.
문제 발생
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> ); }
이런 식으로 코드를 짤 수 있겠죠 ?
코드상으로 보면 딱히 큰 문제가 안 느껴질 수도 있을 거 같습니다.
하지만 이 코드를 실행시켜보면
무한 리렌더링이 발생합니다.
이런 현상이 발생하는 이유는 간단합니다.
- fetchData 실행
- data를 받아와서 setData 실행
- 리렌더링이 발생하여 pagination 객체의 참조 값 변경
- pagination 객체의 참조가 바뀌었기 때문에 useEffect 재실행
- 무한 반복
이런 이유 때문입니다.
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을 사용하는 곳에서 구조 분해 할당을 사용 안 하면 위험할 수 있어서 그렇습니다.
이렇게 수정하고 아까 그 무한 렌더링을 코드를 다시 실행시켜 보면
무한 렌더링이 발생하지 않습니다.