- Published on
Next.js(v14) - 클라이언트/서버에서 쿠키 사용하기 完
시리즈
지난 게시글에서 문제를 해결하지 못하고 대략 1주라는 시간이 지났다. 문제 상황은 정확히 이해했으나, 여전히 해결 방법을 찾지 못했기 때문에 프로젝트 개발 내내 마음 한편에 불편한 존재로 남아있었다.
문제 상황(1편) 요약
📌 서버/클라이언트 요청/응답 과정 중 쿠키에서 값을 꺼낼 때 에러가 발생
우선 내가 찾은 쿠키를 사용 방법은 두 가지인데,
next/headers
사용
1. 내장되어 있는 - 초기 렌더링 시 서버 기능(next/headers에서 제공하는 쿠키 사용 메서드)을 사용할 수 없다는 에러가 발생한다.
- Next 웹 서버로의 요청은 에러가 발생하고 이후 클라이언트 요청은 정상적으로 진행되기 때문에 에러가 발생하지만 동작은 된다.
cookies-next
라이브러리 사용
2. - 초가에 라이브러리를 적용했을 때 별다른 에러 없이 잘 동작됐다.
- 그러나 새로고침 시
accessToken
값이 유효하지 않다는 에러가 발생한다.SSR
시 쿠키에서 값을 꺼내지 못하는 것이다. - 클라이언트에서만 정상 동작
위 두 가지 방법으로는 서버/클라이언트 두 환경에서 내가 원하는 방식으로 쿠키를 사용할 수 없었다.
해결 방안?
해결 방안은 명확하다. 서버/클라이언트
두 요청 과정에서 쿠키를 사용할 수 있는 방법을 찾는 것이다. 앞서 언급한 쿠키 사용방법 외에 다른 방법을 찾아봤으나 별다른 성과는 없었다.
내게 필요한 관련 레퍼런스는 없었고 힘들게 stackoverflow
에서 찾은 한 질문 글에서의 답변은 "Next의 결함인 것 같다."라는 슬픈 답변밖에 없었다.
하지만 운명의 장난일까? 정말 마지막 시도로 며칠 만에 우연히 관련 키워드를 검색하던 중 2023년 10월에 작성된 한 외국 개발자분의 블로그를 발견하게 되었다.
Next.js v13, 쿠키 사진, SSR ... 예감이 좋았다
조금 스크롤을 내리다 보면, 쿠키와 관련된 내용이 언급된다. 나와 비슷한 고민들이 나열되어 있었고 결정적으로 SSR
시 쿠키 액세스에 관한 문제를 지적하고 있었다.
(2023년 기준) 아직 공식적인 해결 방법이 제시되지 않았고 직접 해결할 필요성을 느꼈다는 내용이다. 그리고 이 문제 해결을 위해 직접 만든 라이브러리를 소개해 주신다.
해결
npm install next-client-cookies
or
yarn add next-client-cookies
1. 최상위 or 각 layout.tsx에서 CookiesProvider 적용
import { CookiesProvider } from 'next-client-cookies/server'
export default function RootLayout({ children }) {
return <CookiesProvider>{children}</CookiesProvider>
}
2. 각 코드 로직에 맞게 useCookies 훅 사용
import { VoteDetailType } from '@/app/_types/detailVote.type'
import { useSuspenseQuery } from '@tanstack/react-query'
import { useCookies } from 'next-client-cookies'
import { voteKeys } from '.'
async function fetchVoteDetail(
voteId: number,
accessToken: string,
): Promise<VoteDetailType> {
const res = await fetch(
`${process.env.NEXT_PUBLIC_BASE_URL}/api/votes/${voteId}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
cache: 'no-store',
},
)
const data = await res.json()
if (!res.ok) {
throw Error(data.message)
}
return data
}
export const useVoteDetail = (voteId: number) => {
const cookies = useCookies() ⭐️
const accessToken = cookies.get('accessToken') ?? ''
const {
data: voteData,
isError,
isSuccess,
} = useSuspenseQuery<VoteDetailType, Error, VoteDetailType>({
queryKey: voteKeys.detail(voteId).queryKey,
queryFn: () => fetchVoteDetail(voteId, accessToken),
staleTime: 1000 * 60,
})
return { voteData, isError, isSuccess }
}
결과 비교
❌ before
⭕️ after
fetch 함수 코드 안에서 hook을 사용할 수 없기 때문에 어쩔 수 없이 accessToken을 인자로 넘기는 선택을 하게 되었는데, 이 과정을 더 효율적으로 사용할 수 있도록 추가적인 고민이 필요할 것 같다.
라이브러리 분석해 보기
CookiesProvider
와 useCookies hook
을 보았을 때 쿠키를 상태 관리 라이브러이와 유사하게 전역으로 관리하려는 것 같다는 짐작을 했었는데, Github에서 useCookies hook
코드를 살펴보니 역시나 React Context API
를 사용하고 있었다.
import { useContext, useMemo, useState } from 'react';
import { Cookies } from './types';
import jsCookies from 'js-cookie';
import { Ctx } from './context';
export const useCookies = (): Cookies => {
const ctx = useContext(Ctx);
const [, refresh] = useState(0);
return useMemo((): Cookies => {
const org = typeof window === 'undefined' ? ctx : jsCookies;
if (!org) {
throw new Error('Missing <CookiesProvider>');
}
return {
get: org.get.bind(org),
set: (...args) => {
org.set(...args);
refresh((v) => v + 1);
},
remove: (...args) => {
org.remove(...args);
refresh((v) => v + 1);
},
};
}, [ctx]);
};
useContext
를 사용해 전역 쿠키 관리typeof window === 'undefined' ? ctx : jsCookies
: 서버 사이드 렌더링 환경과 클라이언트 사이드 환경을 구분get
,set
,remove
메서드로 쿠키 조회, 설정, 삭제- 클라이언트에서는 js-cookie 라이브러리를 사용해 쿠키를 관리
const getCookieCommandHtml = (...command: CookieCommand) => (
<script
dangerouslySetInnerHTML={{
__html:`
window.${windowVarName} = window.${windowVarName} || [];
window.${windowVarName}.push(${JSON.stringify(
command,
).replaceAll('</', '<\\/')});
`,
}}
/>
);
코드를 보던 중 낯익은 dangerouslySetInnerHTML
를 발견할 수 있었다. XSS 공격
과 같은 보안 위협을 동반하기 때문에 사용에 있어 주의가 필요한 속성인데 여기서 사용되고 있었다.
dangerouslySetInnerHTML
을 사용해 <script>
태그를 HTML 문서에 직접 삽입하는 방식으로 서버 사이드 렌더링(SSR)
환경에서 클라이언트 사이드에 windowVarName
전역 변수를 사용해 쿠키 관련 명령을 저장하고 실행한다.
간단하게 <script>
태그에 쿠키 저장, 호출 등의 명령어를 저장하고 HTML 문서
에 삽입해 SSR
환경에서도 쿠키를 호출할 수 있던 것이다.
마치며
이렇게 오랫동안 해결하지 못한 문제는 처음이었기 때문에 서버/클라이언트에서의 쿠키 사용 방안은 나에게 항상 아픈 손가락으로 남아있었다. 많은 시간을 투자했고 결국에는 라이브러리로 해결했지만, 이 과정에서 SSR에 대한 견문도 넓힐 수 있었고 지긋지긋한 쿠키를 많이 사용해 볼 수 있었다.
비록 이 라이브러리가 많은 사용자를 보유하고 검증된 라이브러리라고 하기에는 애매한 감이 있지만, 공식 문서와 다른 자료를 찾아봤을 때 뚜렷한 해결 방법을 찾지 못했기 때문에 당장은 이 방법으로 유지할 계획이다.
매번 고통받으며 내가 이 문제를 해결한다면, 반드시 블로그에 글을 써서 이 고통의 굴레를 내 손으로 끊어내겠다는 다짐을 하곤 했었다. 이 라이브러리를 모르는 사람 또한 많으리라 생각하기 때문에 비슷한 고민을 하고 있는 개발자분들이 계시다면 조금이나마 도움이 됐으면 좋겠다.