Next13) 쿠키를 Props처럼 사용

Bie·2023년 12월 24일
//page.tsx

     <Project>
       <ProjectItems id={id} />
     </Project>

클라이언트 컴포넌트에서 서버로 prop을 전달하여 dynamic data fetching을 하고 싶다면 위의 형태로는 불가능하다.

1. 맥락

Project에서 아이콘을 클릭할 때 클릭된 아이콘의 id에 따라 다른 endpoint에서 데이터를 가져오거나 또는 데이터에서 id로 필터링하고 싶다.
(참고로 지금 하고 있는 프로젝트에서는 해당 방법이 오버킬이라 생각하여 부모컴포넌트에서 데이터를 가져와서 클라이언트로 넘겨주는 방법을 사용하였다.)

2. 클라이언트에서 먼저 쿠키 설정

next/header의 쿠키는 서버나 route handler에서만 설정할 수 있다. 때문에 클라이언트에서 쿠키를 설정하게 도와주는 라이브러리를 설치한다. 여러가지가 있는데 나는 next-client-cookies를 사용하였다.

//Project.tsx(Client)
  import { useCookies } from 'next-client-cookies';
  
  const cookies = useCookies();
  
  const onClick = (id: string) => {
   cookies.set('id', id);
   ...
  };

클릭할 때 받은 id를 벨류로 쿠키를 설정했다.
아이콘을 클릭할 때 마다 id쿠키가 계속 새로 설정이 된다.
이렇게 생긴 쿠키를 서버컴포넌트에서 바로 읽을 수 있지만 페이지가 한번 렌더링 된 후 클라이언트에서 아무리 쿠키를 새로 설정해도 페이지를 재렌더링 하지 않는 한 서버에서는 클라이언트에서 재생성된 쿠키를 받을 수가 없었다.

그 이유는 next13에서는 퍼포먼스를 위하여 렌더링 작업을 캐싱하는데, 가져온 데이터 뿐만 아니라 클라이언트의 요청까지도 캐싱한다.

따라서 첫번째 요청때 설정된 쿠키가 캐쉬로 남아 있으니 클라이언트에서 계속 쿠키를 재설정 해도 서버는 캐쉬로 저장된 쿠키만 사용 했던 것이다.

캐싱작업의 장점은 같은 url을 한번만 요청해도 캐쉬로 남아있기 때문에 같은 요청을 다른 컴포넌트에서 해도 데이터 페칭을 하는 것이 아니라 저장된 캐쉬를 사용한다. 따라서 트리 말단에 있는 컴포넌트에서 데이터를 페칭하더라도 다른 레벨의 컴포넌트에서 같은 데이터가 필요할 때 부담없이 페치하여 사용할 수 있다.

따라서 클라이언트에서 설정한 쿠키를 서버에서 받기 위해선 두가지 방법이 있겠다.

1. 요청 캐싱 제외

const { signal } = new AbortController()
fetch(url, { signal })

데이터 페칭 단계에서부터 요청을 캐싱을 금지하는 설정을 위와 같이 해줄 수 있다.

2. 쿠키 설정때마다 렌더링

  const router = useRouter();
  
  const onClick = (id: string) => {
   cookies.set('id', id);
   router.replace(`/projects?id=${id}`);
   ...
  };

useRouter는 클라이언트 사이드 라우팅으로 useRouter로 route을 바꿀때 전체페이지를 재로딩하는 것이 아니라 다른 점만 로딩한다. (여기서는 데이터가 되겠다)

push도 사용할수 있지만 replace는 방문기록에 남지 않고 push는 방문 기록에 남게된다.
또는 router.asPath로 동일경로를 사용해도 된다.

  const handleClose = (id: string) => {
   cookies.remove('id');
   router.replace(`/projects`);
   ...
  };

클릭한걸 닫았을때 id가 필요하지 않으니 쿠키를 삭제하고, 경로에 쿼리가 남아있지 않도록 다시 원상복구해준다.

3. 서버 컴포넌트에서 쿠키 사용하기

//ProjectItems.tsx(Server)
import { cookies } from 'next/headers';

async function ProjectItems() {
  const cookieStore = cookies();
  const id = cookieStore.get('id')?.value
  const projectItems = await basicFetch<ProjectItem>(projectUrl);
  const selectedProject = projectItems[id]
  ...
  
  return (
  	<div>{selectedProject.title}</div>
    ...
  )
}

서버에서는 next/headers나 클라이언트에서 사용했던 라이브러리를 이용하여 설정해둔 쿠키값을 가져온다. 그리고 가져온 데이터에서 쿠키값을 인덱스로 사용하여 특정 데이터만 리턴해 줄 수 있었다.

나는 id마다 별도의 endpoint가 없어서 전체 데이터를 가져온 후 쿠키값으로 특정하여 썼지만 id 별로 endpoint가 각각 있을때 더욱 유용할 것 같다.

그리고 가져온 쿠키를 그대로 사용하지 않고 편집을 해서 사용해야 하는 경우라면 middleware를 사용하면 되겠다.

//middleware.ts

import { NextRequest, NextResponse } from "next/server";

export async function middleware(request: NextRequest) {
  const cookie = request.cookies.get('id')?.value;
  
  const response = NextResponse.next();
  reponse.cookies.set('id', `편집한 값`);
  return response
}

export const config = {
  matcher: '/projects'
}

middleware는 app과 같은 레벨에 있어야 하고 특정 경로에서만 실행하게 할 수도 있다.

경로 실행 순서
1. headers from next.config.js
2. redirects from next.config.js
3. Middleware (rewrites, redirects, etc.)
4. beforeFiles (rewrites) from next.config.js
5. Filesystem routes (public/, _next/static/, pages/, app/, etc.)
6. afterFiles (rewrites) from next.config.js
7. Dynamic Routes (/blog/[slug])
8. fallback (rewrites) from next.config.js

클라이언트에서 요청이 완료되기 전부터 미들웨어가 먼저 실행이 되기 때문에, 예를 들면, 유저가 어드민 페이지나 특정 경로를 요청한다고 할 때 쿠키 값에 따라 입장 또는 다른 경로로 빠르게 보내버릴 수 있을 것이다.


참고
https://nextjs.org/docs/app/building-your-application/routing/middleware#matching-paths
https://nextjs.org/docs/app/building-your-application/caching
https://stackoverflow.com/questions/6922145/what-is-the-difference-between-server-side-cookie-and-client-side-cookie

0개의 댓글