
문제 1 - 쿼리 스트링
기존에는 pagination 페이지에서 쿼리 스트링에 page 값을 저장하여 불러오는 방식으로 구현되어 있었다.
http://localhost:3000/super-admin/user?page=0
해당 page 키 값은 API 통신을 하는 커스텀 훅에서 바꿔주고 있었다.
const response = superAdminApi .get<PaginationResponse<User>>('/users', { params }) .then((res) => { searchParams.set('page', params.page.toString()); setSearchParams(searchParams); return res.data; })
이렇게 하다 보니, 문제가 발생했다. 바로 url이 2번 바뀌게 되는 것이었다.
관리 페이지에서 전체 사용자 조회 페이지로 넘어가는 순간을 살펴보자. url들은 다음과 같다
관리 페이지 URL:
http://localhost:3000/super-admin/manage사용자 조회 URL:
http://localhost:3000/super-admin/user?page=0사용자 조회 클릭 시
super-admin/manage → super-admin/user 로 한 번 바뀌고, 0번 page의 사용자 데이터를 fetch 해오고 setSearchParams로 super-admin/user → super-admin/user?page=0 으로 또 한 번 더 바꿔준다.이렇게 총 2번이 바뀌게 된다. 따라서 사용자가 전체 사용자 조회 화면에서 뒤로 가기 버튼을 “한 번”눌렀을 때,
super-admin/user?page=0 → super-admin/user 로 url이 바뀌게 되므로 의도한 대로 관리 페이지로 나갈 수 없게 된다.해결 방법
위 문제는
setSearchParams에 option 객체로 replace 파라미터를 true로 넘겨주면 해결할 수 있다.(searchParams 는 URLSearchParams 객체를 반환한다.)
setSearchParams(searchParams, { replace: true })
이렇게 함으로써 history 스택에 새 항목을 추가하는 대신 현재 항목을 대체할 수 있다.
이렇게 문제 1은 해결할 수 있었지만, 개발을 하다 보니 또 다른 문제를 발견했다.
문제 2 - API 중복 요청
아뿔싸! api가 2번씩 요청이 가고 있었다.
그 원인을 파악해보니 로직 자체에 문제가 있었다.
SuperAdminWorkspace 컴포넌트에서 useEffect에서 workspace들을 fetch하고 있다. 그리고 searchParams을 의존성 배열에 넣어놓은 상태다.
// SuperAdminWorkspace 컴포넌트 useEffect(() => { const nowPage = Number(searchParams.get('page')); const searchValue = userInputRef.current?.value || ''; fetchAndSetWorkspaces(nowPage, pageSize, searchValue, true); }, [searchParams]);
그리고
fetchAllWorkspaces에서는 쿼리 스트링을 바꾸고 있어서 searchParams가 변하게 되고, 다시 useEffect 내의 fetchAndSetWorkspaces이 호출된다. const fetchAllWorkspaces = (page: number, size: number, name?: string, replace?: boolean) => { . . . const response = superAdminApi .get<PaginationResponse<Workspace>>('/workspaces', { params }) .then((res) => { searchParams.set('page', params.page.toString()); setSearchParams(searchParams, { replace }); return res.data; }) return response; };
이렇게 되면 다음과 같은 무한 API 호출 및 렌더링이 발생하게 된다.
useEffect실행 → fetchAndSet호출 → setSearchParams에서 searchParams변경 → useEffect실행 → fetchAndSet호출 → . . .그러나 그런 일은 일어나지 않았다.
그 이유가 뭘까?
그것은 바로 useSearchParams에서 반환되는 searchParams와 setSearchParams는 각각 useMemo와 useCallBack이 적용되어 있기 때문이다. (useSearchParams 분석)
그래서 나는 운이 좋게도? 2번의 중복된 API호출이 발생하였다.
해당 API 호출의 과정은 다음과 같다.
컴포넌트가 마운트 됐을 때 useEffect실행 → fetchAndSet함수 실행
[1] → setSearchParams로 searchParams변경 (캐싱 시작) → useEffect실행 → fetchAndSet함수 실행[2] → setSearchParams로 seachParams변경 (이전과 동일한 값이므로 캐싱 유지)해결 방법
위 문제는 책임의 소재를 확실히 분리함으로써 해결할 수 있었다.
fetch하는 건 오직 useEffect에서만 하고, fetch를 해야하는 경우(다른 페이지 이동, 검색어 입력)에는 searchParams를 변경하여 useEffect를 트리깅 하는 방식으로 바꿨다.
실제 사용처를 살펴보자
BEFORE
useEffect(() => { const nowPage = Number(searchParams.get('page')); const searchValue = userInputRef.current?.value || ''; fetchAndSetWorkspaces(nowPage, pageSize, searchValue, true); }, [searchParams]);
<Pagination totalPageCount={workspaces.totalPages} paginateFunction={(page: number) => { fetchAndSetWorkspaces(page, pageSize, userInputRef.current?.value); }} />
const SuperAdminSearchBar = forwardRef<HTMLInputElement, SuperAdminSearchBarProps>((props, ref) => { . . . props.fetchContents(0, 6, ref.current?.value, false); };
useEffect에서도 fetch를 하고, Pagination 컴포넌트, 심지어 SuperAdminSearchBar(검색바) 컴포넌트에서도 페이지 클릭 시 fetch를 하고 있다. 이런 구조는 정말 좋아 보이지 않는다.
지금은 운이 좋아서 API 요청이 2번 이루어지는 부작용만 있지만 나중에는 엄청 심각한 결과를 초래할 수 있기 때문이다.
그래서 나는 fetch하는 부분은 useEffect에서만 하고 기존에 fetch를 따로 하던 부분에서는 searchParams를 변경해주는 방식으로 바꿨다.
AFTER
useEffect(() => { const nowPage = Number(searchParams.get('page')); const searchValue = searchParams.get('name') || ''; fetchAndSetWorkspaces(nowPage, pageSize, searchValue); }, [searchParams.toString()]);
<Pagination totalPageCount={workspaces.totalPages} paginateFunction={(page: number) => { searchParams.set('page', page.toString()); setSearchParams(searchParams); }} />
function SuperAdminSearchBar() { . . . const fetchContentsByName = (e: React.KeyboardEvent<HTMLInputElement>) => { if (!(e.key === 'Enter' && inputRef && typeof inputRef !== 'function')) return; searchParams.set('page', '0'); if (inputRef.current?.value === '') { searchParams.delete('name'); } else { searchParams.set('name', String(inputRef.current?.value)); } setSearchParams(searchParams); };
const fetchAllWorkspaces = (page: number, size: number, name?: string) => { const params: FetchAllWorkspacesParamsType = { page, size, name }; const response = superAdminApi .get<PaginationResponse<Workspace>>('/workspaces', { params }) .then((res) => { return res.data; })
이를 통해 책임을 분리하고 좀 더 나은 코드가 되었고, 모든게 정상적으로 작동한다!
후기
해당 문제를 해결하면서 useSeachParams 훅에 대해 분석할 수 있었다. 그리고 문제를 정의하고 해당 문제를 해결하는 과정을 통해 몰랐던 내용들을 많이 배울 수 있었다.
점점 더 깊게 생각하는 태도를 갖추려고 노력하고 있다.

참고링크
react-router/packages/react-router/lib/hooks.tsx at main · remix-run/react-router
Declarative routing for React. Contribute to remix-run/react-router development by creating an account on GitHub.
A Visual Guide to React Rendering - useMemo | Alex Siodrenko
How useMemo can help you prevent unnecessary re-renders.
![[리액트] 깊이알아보는 useState, 리렌더링핵심 작동원리](https://joong-sunny.github.io/assets/images/hyena.jpg)
[리액트] 깊이알아보는 useState, 리렌더링핵심 작동원리
주제