[Next.js] BFF 패턴에서 토큰 처리하고 axios 요청보내기

Rachaen·2023년 5월 14일
2

Next.js에서 소셜 로그인 후 토큰을 저장하는 것에 대한 문제가 발생하였다.
토큰을 BFF에서 사용해야했기 때문에 로컬 스토리지에 저장하여 사용할 수 없는 상황이었다.
그래서 클라이언트에서 받은 토큰을 BFF 서버에서 쿠키로 설정하는 방식을 택하였습니다.

프로젝트에서 pages/api 디렉토리를 사용하여 BFF를 구현하였습니다.

과정

  1. 소셜 로그인 후 redirection 처리
    소셜 로그인이 성공하면, spring 서버는 클라이언트에게 토큰과 함께 리다이렉션 URL을 보냅니다. 이 URL에서 토큰을 추출하고, 이를 BFF 서버에 전송하여 쿠키에 저장하도록 요청합니다.

  2. 토큰 저장
    클라이언트가 보낸 토큰을 쿠키에 저장하는 엔드포인트를 만들었습니다.

  3. 쿠키 파싱
    쿠키를 파싱하고, 토큰을 추출하는 유틸리티 함수를 만들었습니다.

  4. BFF axios 인스턴스 생성
    BFF(Browser-facing server)에서 사용할 요청에 인증 헤더를 자동으로 추가하는 역할을 해줄axios 인스턴스를 생성하였습니다.

  5. BFF에서 axios 인스턴스 사용
    페이지 또는 API 요청에서 BFF axios 인스턴스를 사용하여 백엔드 서비스로 요청을 보냅니다.

  6. 클라이언트에서 axios 인스턴스 사용
    클라이언트에서는 별도의 BFF 엔드포인트에 요청을 보내는 axios 인스턴스를 생성하고 사용합니다.


소셜로그인 Redirect 페이지

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;
  • 소셜 로그인 후 redirect된 URL에서 토큰을 추출하고, 이 토큰을 서버에 전달하여 쿠키에 저장시키기 위한 api 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' });
  }
}
  • 요청에서 토큰을 가져와서 HTTP 응답에 Set-Cookie 헤더를 설정하여 쿠키에 토큰을 저장합니다.

쿠키 파싱

// 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;
    }, {});
}
  • 쿠키에서 토큰을 가져오기 위한 함수입니다.

BFF axios 설정

// 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;
}
  • 이 함수는 HTTP 요청을 보내기 전에 인증 헤더를 추가하는 axios 인스턴스를 생성합니다.
  • 요청을 보내기 전에 쿠키를 파싱하여 토큰을 가져옵니다.

기본적인 것 세팅(?)은 이렇게 됩니다.
이제 활용을 해봅시다.

BFF axios 인스턴스 사용

// 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' });
  }
}
  • BFF axios 인스턴스는 서버에서 각종 API 요청을 처리할 때 사용됩니다. 이를 통해 쿠키에 저장된 토큰을 인증 헤더에 자동으로 추가하여 요청을 보낼 수 있습니다.

getServerSideProps에서 BFF axios 사용

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;
  • getServerSideProps 함수에서 BFF axios 인스턴스를 사용하여 서버 사이드 렌더링을 수행합니다.
  • 페이지가 렌더링되기 전에 데이터를 가져오는 데 사용됩니다. 데이터는 페이지 컴포넌트에 props로 전달됩니다.

클라이언트에서 axios 사용을 위한 인스턴스

export default function createClientHttpInstance() {
  const instance = axios.create({
    baseURL: '/api',
  });

  return instance;
}
  • 클라이언트에서 직접 API 요청을 보내기 위해 별도의 axios 인스턴스를 생성합니다. 이 인스턴스는 클라이언트에서 BFF로 요청을 보내는데 사용됩니다.

클라이언트에서 axios 인스턴스 사용

const clientHttp = createClientHttpInstance();

clientHttp
  .get('/BFF-endpoint')
  .then(response => {
    const { data } = response;
    // 응답 처리...
  })
  .catch(error => {
    console.error(error);
    // 에러 처리...
  });
  • 클라이언트는 /api/BFF-endpoint를 통해 BFF에 요청하고, BFF는 이 요청을 백엔드 서비스로 전달한 후 응답을 클라이언트에 반환합니다.

BFF 패턴을 적용하는 걸 너무 쉽게 본 듯 하다...
api 모듈화하는 것과 비슷할 줄 알았는데 약간 더 신경써야 할 점들이 있는 것 같다.

profile
개발을 잘하자!

1개의 댓글

comment-user-thumbnail
2023년 6월 7일

감사합니다^^

답글 달기