Next-js App-router 서버에서 React Query 사용하기

RN·2024년 8월 27일

리액트

목록 보기
4/8

앱 라우터 이전의 페이지 라우터에서의 리액트 쿼리 사용법은 웹상에 많았다.

__App.tsx 라는 앱 로더를 사용하는데 앱 라우터에서는 이 __App.tsx가 없다.

그래서 공식문서를 통해 공부할 수 밖에 없었다.


1. 초기 설정



Next.js 앱 라우터에서는 기본적으로 컴포넌트가 서버 컴포넌트이다.

이 서버 컴포넌트는 서버에서 동작하는데, React Query를 사용할 수 없다.

그래서 RootLayout 에서 바로 QueryClientProvider를 사용하지 않고,

'use client'를 사용한 클라이언트 컴포넌트를 이용하여 한 번 덮어 씌워준다.


위에서 녹색 박스는 브라우저가 아직 query client가 없는 경우 새 client를 만들어주는데, 이렇게하면 초기 렌더링 중에 React가 일시 중단되더라도 새 client를 다시 만들지 않는다. query client 생성 아래에 suspense boundary 가 있는 경우 이 작업이 필요하지 않을 수 있다.




2. de/hydration


우선 들어가기전에 dehydrationhydration 개념을 알아야한다.

hydrate라고 한다면 Next.js 를 사용해본 사람이라면 들어본 개념이다.

SSR 에서 렌더링된, 정적이며 사용자와의 상호작용이 불가능하여 말라있는 HTML 페이지에 javascript(.js 파일) 라는 수분을 공급하여 동적이며 사용자가 사용할 수 있는 페이지를 완성하는 방법이다.

그래서 SSR 프레임워크인 Next.js 를 사용하면 서버에서 pre-rendering 된 페이지를 미리 사용자에게 보여주어 빈 페이지를 보여주지 않아 사용자 경험을 높이며, 직후 번들된 js 파일을 받아 동적인 페이지를 받을 수 있다.

이로 인해 사용자는 pre-rendering 된 페이지를 미리 받아볼 수 있고, 검색 엔진 크롤러에게도 pre-rendering 된 페이지가 전달된다.



그렇다면 dehydrate는 반대로 동적인 페이지를 정적인 페이지로 바꾼다는 뜻일 것이다.

서버에서 React query를 사용할 때는 de/hydrate를 아래와 같이 사용한다고 한다.

서버에서는 마크업(여기서는 HTML)을 생성/렌더링하기 전에 데이터를 prefetch해야 하고, 마크업에 내장할 수 있는 직렬 형식으로 데이터dehydrate해야 하며, 클라이언트에서는 해당 데이터를 react query cachehydrate하여 클라이언트에서 새로운 fetch를 하지 않도록 해야 한다.

next.js 에서 js 파일HTML 파일에 뿌리는 것을 hydrate라고 했다.

위의 예시에서는 prefetch 한 데이터클라이언트에서 리액트 쿼리 캐시에 뿌리는 것을 hydrate 라고 한다.

여기서 같은 색깔이 서로 대응되는 느낌이다.




3. useQuery prefetch & de/hydrate


데이터가 서버에서 잘받아와지는 지 확인하기 위하여 json-server를 사용하여 아래의 데이터를 사용한다.

위의 데이터는 해당 날짜의 웹툰 플랫폼별 실시간 인기순위 웹툰들이다.

json-server를 실행하여 localhost:3000/웹툰 URL 에서 데이터를 받아올 수 있다.

json-server 를 사용하는 방법은 매우 간단하니 처음이라도 쉽게 사용할 수 있다.

한 번 서버에서 데이터를 받아오는지 확인해보자.

prefetchQuery를 통해 미리 데이터를 fetch 해놓고 동일한 queryKey를 이용한 쿼리(useQuery나 useSuspenceQuery)를 사용하면 새로 서버에 데이터를 호출할 필요가 없다.

그리고 HydrationBoundarydehydrate된 데이터를 전달할 컴포넌트(여기서는 Webtoon 컴포넌트)를 감싼다.

HydrationBoundary는 prefetch 하는 컴포넌트마다 전부 사용해도 상관 없다고 한다.

getWebtoon API는 json-server를 통해 데이터가 제대로 오는지 확인하고 prefetch 하기 위해 작성했다.

서버에서 제대로 된 데이터가 출력되는 것을 확인했다.

이제 클라이언트 컴포넌트인 Webtoon 컴포넌트에서 useQuery를 사용하여 데이터를 받아온다.

위와 같이 제대로 출력되는 것을 알 수 있다.


하지만 이걸로는 prefetch 되는지 정확히 알 수 없다.

위의 사진에서 서버에서 출력되는 데이터도 react query가 아니라 단순히 fetch API를 사용해서 fetch().then() 에서 받아온 데이터를 출력한 것이지, prefetch 를 통해 데이터를 미리 받아온건지는 판단할 수 없다.


그래서 아래와 같이 prefetch 하지 않고 바로 useQuery를 사용한 페이지를 만들었다.

헷갈릴까봐 찍었는데 아래는 현재 프로젝트의 디렉터리 구조이다.

아래의 사진처럼 csrwebtoon 폴더에 새로운 페이지를 만들었다.

이대로 출력하면 아래와 같은 결과가 나온다.

localhost:3001/csrwebtoon 에서 네트워크 탭을 확인하면 위와 같이 fetch가 동작하여 데이터를 받아온다.


하지만 위에서 했던 prefetch 한 페이지(localhost:3001/webtoon)에서는 아래와 같은 결과가 나온다.

fetch 없이 데이터를 잘받아오는 것을 알 수 있다.




4. useInfiniteQuery prefetch & de/hydrate


[
    {
        "Platform" : "네이버",
        "Ranking" : [
            { "Title" : "화산귀환", "Genre" : "무협", "Rank" : "1"},
            { "Title" : "외모지상주의", "Genre" : "액션", "Rank" : "2"},
            { "Title" : "소꿉친구 컴플렉스", "Genre" : "로맨스", "Rank" : "3"}  
        ]
    },
    {
        "Platform" : "카카오",
        "Ranking" : [
            { "Title" : "우리 악녀님이 달라졌어요", "Genre" : "로맨스 판타지", "Rank" : "1"},
            { "Title" : "후작가의 역대급 막내아들", "Genre" : "판타지", "Rank" : "2"},
            { "Title" : "얘 우리 딸 아니에요!", "Genre" : "로맨스 판타지", "Rank" : "3"}
        ]
    },
    {
        "Platform" : "리디",
        "Ranking" : [
            { "Title" : "상수리나무 아래", "Genre" : "로맨스 판타지", "Rank" : "1"},
            { "Title" : "내게 빌어봐", "Genre" : "로맨스 판타지", "Rank" : "2"},
            { "Title" : "프리즌 러브", "Genre" : "로맨스 판타지", "Rank" : "3"}
        ]
    },
    {
        "Platform" : "레진",
        "Ranking" : [
            { "Title" : "브리아노의 연구소", "Genre" : "개그", "Rank" : "1"},
            { "Title" : "낮에 뜨는 별", "Genre" : "로맨스", "Rank" : "2"},
            { "Title" : "화우요", "Genre" : "BL", "Rank" : "3"}
        ]
    },
    {
        "Platform" : "케이툰",
        "Ranking" : [
            { "Title" : "악마도 의무교육 받습니다!", "Genre" : "판타지", "Rank" : "1"},
            { "Title" : "리더(Reader)-읽는자", "Genre" : "판타지", "Rank" : "2"},
            { "Title" : "살인마VS이웃", "Genre" : "미스터리", "Rank" : "3"}
        ]
    },
    {
        "Platform" : "피너툰",
        "Ranking" : [
            { "Title" : "그저 여명일 뿐", "Genre" : "로맨스", "Rank" : "1"},
            { "Title" : "안개 바다 위의 샤베트", "Genre" : "GL", "Rank" : "2"},
            { "Title" : "레가스", "Genre" : "BL", "Rank" : "3"}
        ]
    },
    {
        "Platform" : "봄툰",
        "Ranking" : [
            { "Title" : "시금치 꽃다발", "Genre" : "BL", "Rank" : "1"},
            { "Title" : "이웃집 길드원", "Genre" : "BL", "Rank" : "2"},
            { "Title" : "페이백(PAYBACK)", "Genre" : "BL", "Rank" : "3"}
        ]
    },
    {
        "Platform" : "버프툰",
        "Ranking" : [
            { "Title" : "제카툰", "Genre" : "개그", "Rank" : "1"},
            { "Title" : "겜덕툰", "Genre" : "일상", "Rank" : "2"},
            { "Title" : "팬피터", "Genre" : "판타지", "Rank" : "3"}
        ]
    },
    {
        "Platform" : "큐툰",
        "Ranking" : [
            { "Title" : "내 곁에 너를", "Genre" : "로맨스", "Rank" : "1"},
            { "Title" : "페로몬 도착증", "Genre" : "BL", "Rank" : "2"},
            { "Title" : "이세계에서 로그아웃하는 방법", "Genre" : "판타지", "Rank" : "3"}
        ]
    }
]









해당 데이터를 webtoon.json 파일로 만들고 위의 getWebtoons API를 만들어 useInifiniteQuery로 읽어보겠다.

맨 위의 두 줄은 webtoon.json 파일을 읽기 위한 코드이다.

dehydrate 한 데이터에 .queries[0].state.data 를 이용하여 접근한 후 우리가 dehydrate한 데이터를 서버에서 그대로 출력한다.

pageParam 3에 해당하는 webtoon.json 의 4번째(인덱스 3) 데이터 "레진" 에 대한 정보가 들어온 것을 확인할 수 있다.


보통 prefetchInifiniteQuery는 하나의 데이터만 미리 가져온다고 하는데, 여러 페이지를 아래처럼 한 번에 받아올 수도 있다.

여러 개의 데이터를 한 번에 받기위해서는 pagesgetNextPageParam 속성이 추가로 필요하다.

pages에는 몇 개의 데이터를 prefetch 할지, getNextPageParam 은 다음 페이지를 가져올 queryFn 에 할당할 pageParam 을 전달해야 하기 때문에 당연히 필요하다.

lastPage는 마지막에 가져온 페이지이기 떄문에 3개의 데이터를 가져올 때

처음 queryFn -> getNextPageParam -> queryFn -> getNextPageParam -> queryFn 을 실행하며 직전 queryFn을 실행하여 얻은 마지막 데이터를 출력한다.

이때 각 getNextPageParam에서 반환하는 값을 queryFn의 pageParam 매개변수로 사용한다.


어쨌든 위의 결과에서 0 번째부터 3개의 데이터를 받아오는 것을 볼 수 있다.


4. 서버 컴포넌트 스트리밍


스트리밍이란?

스트리밍은 라우트를 더 작은 "Chunk" 로 나누고 데이터가 준비되면 서버에서 클라이언트로 점진적으로 스트리밍할 수 있는 데이터 전송 기술입니다.

Next.js 앱 라우터는 페이지 단위로 스트리밍한다.

스트리밍을 통해 느린 데이터 요청이 전체 페이지를 차단하는 것을 방지할 수 있다.

이를 통해 사용자는 UI를 사용자에게 표시하기 전에 모든 데이터가 로드될 때까지 기다리지 않고도 페이지의 일부를 보고 상호 작용할 수 있다.

https://nextjs.org/learn/dashboard-app/streaming

React의 Suspense 나 Next.js 의 loading.tsx 를 사용하여 스트리밍을 구현할 수 있다.





0개의 댓글