[올해도 아좌좌] JWT 쿠키에 저장하기

ahyes·2023년 11월 14일
0

올해도 아좌좌

목록 보기
1/4

왜 쿠키를 사용하는가?

올해도 아좌좌 프로젝트를 진행하던 중 기존 리액트 프로젝트 처럼 JWT 토큰을 로컬스토리지에 저장했다. 하지만 Next.js로 프로젝트를 만들고있기 때문에 로컬스토리지에 저장된 토큰은 서버 컴포넌트에서 접근하지 못했다...!

그리고 추가적인 문제가 있는데, 만약 자동 로그인 기능을 사용하려 한다면 로컬 스토리지에 접근해 토큰을 가져와야 로그인이 되어있다는 것을 알 수 있다. 하지만 Next가 렌더링시 window에 바로 접근이 불가능한데 이 때 Recoil에 토큰의 기본값을 null로 설정해 놓은 상태에서 API를 불러오면 Authorization에 들어가는 토큰값이 null로 들어가는 문제가 생긴다 😭
(+ 서버 컴포넌트에선 로컬스토리지에 접근할 수 없다. Recoil에 저장한 전역 변수에도 접근 불가!)

하지만! 쿠키에 JWT 토큰을 저장해놓는다면 서버 컴포넌트와 클라이언트 컴포넌트에서 모두 접근할 수 있습니다. 🙌

그렇기 때문에 바로 이동하는 작업을 착수해보겠습니다.

1. 로그인 할 때 받아오는 JWT 토큰 쿠키에 저장하기

Next.js 공식문서에 따르면 쿠키를 사용하는 방법은 아래와 같다!

import { cookies } from 'next/headers'
 
async function create(data) {
  cookies().set('name', 'lee')
  // or
  cookies().set('name', 'lee', { secure: true })
  // or
  cookies().set({
    name: 'name',
    value: 'lee',
    httpOnly: true,
    path: '/',
  })
}

secure 옵션은 https가 아닌 통신에서 쿠키가 탈취되는 것을 막기 위해 사용할 수 있는 옵션이다.
httpOnly 옵션은 브라우저에서 해당 쿠키로 접근할 수 없도록 만드는 옵션으로 CSS(Cross Site Scripting) 공격을 막기위해 사용할 수 있다.
path의 경우는 특정 디렉토리, 경로에서만 쿠키를 사용하고싶다면 해당 옵션을 사용할 수 있다.

다만 주의할 점은 'next/headers'에 있는 cookies는 서버 컴포넌트에서만 사용할 수 있으니 주의하도록 합니다.

그래서 저는 클라이언트 컴포넌트에서 사용하기 위해 cookies-next를 다운받아 사용했습니다. (SSR에서도 사용 가능)

import { deleteCookie, getCookie, setCookie } from 'cookies-next';
import { AtomEffect, atom } from 'recoil';

export interface auth {
  accessToken: string;
  refreshToken: string;
}

const cookiesEffect: <T>(key: string) => AtomEffect<T> =
  (key: string) =>
  ({ setSelf, onSet }) => {
    const savedValue = getCookie(key);
    if (savedValue !== undefined) {
      setSelf(JSON.parse(savedValue));
    }

    onSet((newValue) => {
      if (newValue === null) {
        deleteCookie(key);
        return null;
      }

      return setCookie(key, JSON.stringify(newValue));
    });
  };

export const authStore = atom<auth | null>({
  key: 'authState',
  default: null,
  effects: [cookiesEffect<auth | null>('auth')],
});

2. axiosInstance 만들기

저는 여기서 interceptors를 사용해 API를 사용할 때 bearer 토큰을 config.headers.Authorization 설정 해주기로 했습니다.

  • interceptor : then 또는 catch로 처리되기 전에 요청과 응답을 가로챌수 있음
import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios';
import { cookies } from 'next/headers';

export const axiosInstance = axios.create({
  baseURL: process.env.NEXT_PUBLIC_TEST_API_END_POINT,
  timeout: 10000,
  authorization: true,
});

axiosInstance.interceptors.request.use(
  (config: InternalAxiosRequestConfig) => {
    if (
      !config.authorization ||
      !config.headers ||
      config.headers.Authorization
    )
      return config;

    const auth = cookies().get('auth');

    if (!auth) {
      throw new Error('토큰이 존재하지 않습니다');
    }

    config.headers.Authorization = `Bearer ${
      JSON.parse(auth?.value).accessToken
    }`;
    return config;
  },
  (error: AxiosError) => {
    return Promise.reject(error);
  },
);

여기서 좀 특이한 부분은 authorization : boolean에 따라 로그인이 필요한 API, 필요하지 않은 API를 구분했습니다.

authorization은 기존 axios에 존재하지 않는 커스텀 설정 옵션입니다.

import 'axios';

declare module 'axios' {
  export interface AxiosRequestConfig {
    authorization?: boolean;
  }
}

이 코드를 .ts 파일에 넣어 적당한 곳에 위치시키면 됩니다.
이름은 자유롭게 정해도 되지만 저는 index.d.ts로 저장했습니다.

3. 사용하기

마지막으로 사용하는 방법은 간단합니다.

사용하고싶은 곳에서 만들어준 인스턴스를 import해 사용하면 끝입니다!

import { axiosInstance } from '@apis/axiosInstance';

export const getPublicPlans = async () => {
  const { data } = await axiosInstance.get('/mock/plans', {
      authorization: false,
  });
  return data;
};
  • 추가로 생각해보기
    • 인터셉트 Response는 언제 사용할까?
    • 인터셉트 Request에서 Error에 어떤걸 추가할까?
profile
티스토리로 이사갑니다. https://useyhnha.tistory.com/

0개의 댓글

관련 채용 정보