Next.js에서 소셜 로그인 후 토큰을 저장하는 것에 대한 문제가 발생하였다.
토큰을 BFF에서 사용해야했기 때문에 로컬 스토리지에 저장하여 사용할 수 없는 상황이었다.
그래서 클라이언트에서 받은 토큰을 BFF 서버에서 쿠키로 설정하는 방식을 택하였습니다.
프로젝트에서 pages/api 디렉토리를 사용하여 BFF를 구현하였습니다.
소셜 로그인 후 redirection 처리
소셜 로그인이 성공하면, spring 서버는 클라이언트에게 토큰과 함께 리다이렉션 URL을 보냅니다. 이 URL에서 토큰을 추출하고, 이를 BFF 서버에 전송하여 쿠키에 저장하도록 요청합니다.
토큰 저장
클라이언트가 보낸 토큰을 쿠키에 저장하는 엔드포인트를 만들었습니다.
쿠키 파싱
쿠키를 파싱하고, 토큰을 추출하는 유틸리티 함수를 만들었습니다.
BFF axios 인스턴스 생성
BFF(Browser-facing server)에서 사용할 요청에 인증 헤더를 자동으로 추가하는 역할을 해줄axios 인스턴스를 생성하였습니다.
BFF에서 axios 인스턴스 사용
페이지 또는 API 요청에서 BFF axios 인스턴스를 사용하여 백엔드 서비스로 요청을 보냅니다.
클라이언트에서 axios 인스턴스 사용
클라이언트에서는 별도의 BFF 엔드포인트에 요청을 보내는 axios 인스턴스를 생성하고 사용합니다.
import React, { useEffect } from 'react';
import { useRouter } from 'next/router';
import Container from '@/components/Container';
import useStore from '@/hooks/memberHook';
import { getMember } from '@/utils/api/member';
import clientHttp from '@/utils/clientHttp';
const Redirect = () => {
const router = useRouter();
const { login, setMemberId } = useStore();
const extractTokenFromUrl = url => {
const urlParams = new URLSearchParams(url.split('?')[1]);
return urlParams.get('token');
};
const handleTokenResponse = async token => {
if (token) {
const response = await clientHttp.get(`/set-token?token=${token}`);
if (response.data.success) {
const data = await getMember();
if (data.isJoined) {
login(data);
} else {
setMemberId(data);
}
const destination = data.isJoined ? '/business' : '/business/join';
router.push(destination);
} else {
router.push('/');
}
} else {
router.push('/');
}
};
useEffect(() => {
const token = extractTokenFromUrl(router.asPath);
handleTokenResponse(token);
}, []);
return <Container />;
};
export default Redirect;
set-token
을 호출합니다.// pages/api/set-token.js
export default function handler(req, res) {
const { token } = req.query;
if (token) {
res.setHeader(
'Set-Cookie',
`token=${token}; HttpOnly; Path=/; Max-Age=${
7 * 24 * 60 * 60
}; Secure; SameSite=Lax`,
);
res.status(200).json({ success: true });
} else {
res.status(400).json({ message: 'Token not provided' });
}
}
// src/utils/parseCookies.js
function parseCookies(cookie = '') {
return cookie
.split('; ')
.map(v => v.split('='))
.reduce((acc, [k, v]) => {
acc[k.trim()] = decodeURIComponent(v);
return acc;
}, {});
}
// utils/http.js
import parseCookies from './parseCookies';
export default function createBffAxiosInstance(req) {
const instance = axios.create({
baseURL: '스프링 서버 URL',
});
instance.interceptors.request.use(
async config => {
const cookies = parseCookies(req.headers.cookie);
const { token } = cookies;
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
error => {
return Promise.reject(error);
},
);
return instance;
}
기본적인 것 세팅(?)은 이렇게 됩니다.
이제 활용을 해봅시다.
// pages/api/XXX.js
export default async function handler(req, res) {
const bffAxios = createBffAxiosInstance(req);
try {
const response = await bffAxios.get('/요청 보낼 endpoint');
res.status(200).json(response.data);
} catch (error) {
res.status(500).json({ message: 'An error occurred' });
}
}
export async function getServerSideProps(context) {
const { req } = context;
const bffAxios = createBffAxiosInstance(req);
try {
const response
const response = await bffAxios.get('/your-endpoint');
const { data } = response;
// data를 props로 전달합니다.
return { props: { data } };
} catch (error) {
// 에러 처리를 적절하게 수행합니다.
console.error(error);
return { props: { data: null, error: 'An error occurred' } };
}
}
const YourPage = ({ data, error }) => {
// 페이지 렌더링...
};
export default YourPage;
export default function createClientHttpInstance() {
const instance = axios.create({
baseURL: '/api',
});
return instance;
}
const clientHttp = createClientHttpInstance();
clientHttp
.get('/BFF-endpoint')
.then(response => {
const { data } = response;
// 응답 처리...
})
.catch(error => {
console.error(error);
// 에러 처리...
});
BFF 패턴을 적용하는 걸 너무 쉽게 본 듯 하다...
api 모듈화하는 것과 비슷할 줄 알았는데 약간 더 신경써야 할 점들이 있는 것 같다.
감사합니다^^