개발 환경
- Next.js 14 (page router)
- TailwindCSS
- Axios
문제 상황
BE에서 API를 통해 JWT을 보내주어 AccessToken을 통해 사용자 인증을 하는 방식을 사용하고 있으며, 코드잇 부트캠프 내에서 하는 프로젝트이기에 BE API를 변경할 수 없었고, RefreshToken 없이 AccessToken만을 사용해 인증을 하고, AccessToken의 주기가 짧아 자주 로그인을 해야했습니다.
또한, 현재 웹스토리지(LocalStorage)를 사용하여 SSR(서버사이드 렌더링) 시 로그인(사용자 인증)이 필요한 서비스(API)는 사용할 수 없었습니다.
- 문제점 요약
- 짧은 토큰 수명때문에 API 통신에 있어 401(UnAuthorized)에러가 자주 발생
- SSR 방식을 사용하기 위해 서버와 클라이언트에서 모두 접근할 수 있는 곳에 토큰을 저장하여 접근해야함
- 프로젝트 진행 중이기에, 다른 팀원들의 코드 변경 없이 스토리지 변경
문제 해결을 위한 고민
1. 토큰 내에 저장되어 있는 수명을 보고 토큰이 만료되면 로그아웃을 하자!
- 토큰 만료시간이 되면 자동으로 삭제해주는 쿠키를 사용하여 토큰을 관리하면, 서버와 클라이언트에서 모두 접근 가능하여 필요한 곳에서 사용할 수 있다.
- 토큰이 쿠키에 저장되어 있는지 확인만하면 현재 사용자가 로그인(사용자 인증)이 되어있는지 확인할 수 있어 추가 로직이 필요하지 않다.
2. 쿠키 말고 다른 방법은 없을까?
- 개별 데이터 베이스를 두어 토큰을 관리하자. : 토큰만을 위한 추가적인 서버가 필요하며, 용량이 커지면 추가적인 비용 지불이 필요하다.(추가적으로 데이터 베이스에 저장할 데이터가 없음)
- 웹스토리지(로컬, 세션)에 저장하면 서버에서 토큰에 접근할 수 없어 제한이 된다.
3. 쿠키를 만들 순 없을까?
- 클라이언트에서 쿠키를 만들 수 있으며, 관련된 라이브러리(react-cookie, cookie 등..)가 많아서 만드는데 문제가 없다.
- Next.js의 API routes를 활용하여 서버에서 httponly 등의 서버에서만 지정 가능한 설정도 만질 수 있다!
4. httponly를 쓸 순 없을까?
- 클라이언트에서 접근하지 못하도록 막아서 보안을 높을 수 있음
- Next.js의 API routes를 활용해서 토큰을 관리하는 API를 만들어 사용하자.
구현 코드
토큰 저장 API
// src/pages/api/save-token.ts
const expireOffeset = 7200; // 2시간
const saveTokenInCookie = async (req: NextApiRequest, res: NextApiResponse) => {
// ...
const accessToken = req.body.accessToken; // accessToken을 request에서 가져옴
if (accessToken !== req.cookies.accessToken) {
// 헤더에 Set-Cookie설정을 넣어 브라우저가 자동으로 쿠키를 저장할 수 있도록함
res.setHeader('Set-Cookie', [
`accessToken=${accessToken}; max-age=${expireOffeset}; path=/; httponly; secure; sameSite=lax;`,
]);
// ...
토큰 삭제 API
// src/pages/api/delete-token.ts
const deleteTokenFromCookie = async (
req: NextApiRequest,
res: NextApiResponse,
) => {
// ...
// 쿠키의 수명을 0으로 설정하여 삭제
res.setHeader('Set-Cookie', ['accessToken=deleted; Max-Age=0; path=/']);
// ...
토큰 가져오기 API
// src/pages/api/get-token.ts
const getTokenFromCookie = async (
req: NextApiRequest,
res: NextApiResponse,
) => {
const accessToken = req.cookies.accessToken;
if (accessToken) {
res.status(200).json({ accessToken });
} else {
res.status(401).json({ message: '로그인이 필요한 서비스 입니다.' });
}
// ...
};
export default getTokenFromCookie;
Axios interceptor 안에 로직 삽입
// axios instance
const onRequest = async (config: InternalAxiosRequestConfig) => {
if (process.env.NODE_ENV !== 'production') {
console.log(config);
}
if (!config.url?.startsWith('/auth')) {
try {
const data = await axios.get('/api/get-token/cookie');
if (data.data.accessToken) {
const accessToken = data.data.accessToken;
if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
}
} catch {}
}
// ...
const onResponse = async (value: AxiosResponse) => {
if (value.data?.accessToken) {
try {
const data = await axios.post(
'/api/save-token/cookie',
{
accessToken: value.data.accessToken,
},
{
headers: {
'Content-Type': 'application/json',
},
},
);
// ...
return value;
};
// ...
instance.interceptors.request.use(onRequest, onError);
instance.interceptors.response.use(onResponse, onError);
작동 테스트
로그인
(로그인 API 응답 -> axios interceptor response -> cookie 저장 )
로그아웃
(로그아웃 -> 쿠키 삭제 api route 호출)
게시글 조회
(폴더와 링크 조회 API 호출-> Axios interceptor request -> 쿠키 가져와서 header에 파싱 -> API 호출 계속)
마무리
제한된 상황에서 내가 할 수 있는 것들은 무엇인가 확인하고, FE/BE를 구분하지 않고 더 좋은 서비스를 위해 고민한 경험이었고,
"유저 정보를 저장하는 방식으로 세션과 쿠키 방식이 있다" 라고 공부는 했었지만, 두 방식의 장단점을 비교하면서 자주 사용해볼 기회가 없었던, 또 직접 만들어본 경험이 없었던 쿠키를 다루어 볼 수 있는 경험이 되어 알찼습니다.
(+ 추가 해결해볼 문제)
로그인을 한 후에, 사용자가 페이지에서 떠난 후 다시 돌아왔을 때 쿠키의 만료 시간 내면 자동 로그인이 되어야하는데
메인 페이지에 있는 Navigation Bar 안 로그인 버튼이 깜빡거리는 버그를 수정해볼 생각입니다.
전체코드
'PROJECT > 에러핸들링' 카테고리의 다른 글
[OPENMIND] 반응형 리스트 요소 개수 개선 - UX (3) | 2024.07.26 |
---|