본문 바로가기
React

JWT를 이용한 사용자 정보 SSR (next.js와 redux-saga 사용)

by Luke K 2022. 8. 7.

이 글은 직접 프로젝트에서 적용한 react와 next를 사용한 프로젝트에서 jwt를 이용하여 사용자 정보를 서버사이드 렌더링 하는 것을 다룬다.

대게 웹에서 jwt를 다룰 때 로컬스토리지나 세션 스토리지를 사용하곤 한다.

하지만 ssr을 이용하게 되면 프론트 서버는 로컬스토리지가 없어 사용자의 정보를 가져올 수가 없다.

그렇기에 나의 경우는 쿠키로부터 토큰을 관리하는 방식을 사용했다.

토큰을 관리하는 과정을 정리하면

  1. 로그인하면 액세스 토큰과 리프레쉬 토큰을 받는다.
  2. 쿠키와 리덕스에 두 토큰을 넣어놓는다.
  3. 헤더에 액세스 토큰을 붙여놓는다.
  4. 새로고침 시 프론트 서버에 있는 쿠키를 바탕으로 내 정보를 불러온다.

리덕스 사가를 이용한 로그인 코드이다

function* logIn(action) {
  try {
    const result = yield call(logInAPI, action.data);
    yield put({
      type: LOG_IN_SUCCESS,
      data: result.data.
    });
    axios.defaults.headers.common["x-access-token"] =
      result.data.accessToken;

    cookie.save("accessToken", accessToken, {
      path: "/",
    });
    cookie.save("refreshToken", refreshToken, {
      path: "/",
    });
  } catch (err) {
    console.error(err);
    yield put({
      type: LOG_IN_FAILURE,
      error: err.response.data,
    });
  }
}

코드를 설명하면 로그인 요청이 되면 시작되는 코드인데

  1. 성공 시 데이터를 리덕스 스토어에 넣는다
  2. 앞으로 서버로 정보를 요청할 때 기본적으로 헤더에 access-token이 들어가게끔 한다.
  3. 쿠키에 토큰을 저장한다.

이걸 이용해서 서버 사이드 렌더링을 하는 코드를 살펴보면

export const getServerSideProps = wrapper.getServerSideProps(
  async (context) => {
    const parsedCookie = context.req
      ? cookie.parse(context.req.headers.cookie || "")
      : "";
    if (context.req && parsedCookie) {
      if (parsedCookie["accessToken"]) {
        context.store.dispatch({
          type: LOAD_MY_INFO_REQUEST,
          data: parsedCookie["accessToken"],
        });
      }
    }
    context.store.dispatch(END);
    await context.store.sagaTask.toPromise();
  }
);

프론트 서버에 있는 쿠키를 이용하여 내 정보에 대한 요청을 보내는 코드이다.

사용자 정보를 요청하는 비동기 코드를 알아보면

function loadMyInfoAPI(data) {
  return axios.get("/auth/me", {
    headers: {
      "x-access-token": data,
    },
  });
}

위에 서버사이드 렌더링을 하는 코드에서 쿠키에 있는 토큰을 넣어주면 헤더에 그 토큰을 다시 넣고 정보를 요청한다.

그리하여 받는 나의 정보를 다시 저장한다.

function* loadMyInfo(action) {
  try {
    const result = yield call(loadMyInfoAPI, action.data);
    yield put({
      type: LOAD_MY_INFO_SUCCESS,
      data: result.data.data,
    });

    cookie.save("accessToken", accessToken, {
      path: "/",
    });
    cookie.save("refreshToken", refreshToken, {
      path: "/",
    });
  } catch (err) {
    console.error(err);
    yield put({
      type: LOAD_MY_INFO_FAILURE,
      error: err.response.data,
    });
  }
}

이러한 방식을 이용하면 만료가 되지 않는 토큰의 관리를 할 수 있다.

하지만 리프레쉬 토큰을 이용해서 만료된 액세스 토큰을 바꿔올 수 없는 문제가 있다.

refreshToken만 있을경우에 유저 정보 Server Side Rendering

유저의 정보를 서버사이드 렌더링 할 때, 만료되지 않은 액세스 토큰이 있다면, 사용자 정보 요청 후 리덕스에 담아주는 방법으로 문제를 풀었다면 액세스 토큰이 만료됐고 리프레쉬 토큰만 남아있다면 어떻게 사용자 정보를 ssr할 수 있을 지 알아보겠다.
액세스 토큰과 리프레쉬 토큰을 확인하여 유저 정보를 ssr하는 과정은

  1. context.req.headers에 쿠키가 있는 지 확인한다.
  2. 액세스 토큰이 있다면 이를 이용해 정보를 불러온다
  3. 액세스 토큰이 없고 리프레쉬 토큰만 있다면 토큰도 재발급 받고, 사용자 정보를 재요청한다.
  4. 받아온 정보들을 리덕스와 cookie에 세팅한다.

이런식의 코드를 내정보를 사용해야하는 페이지 하단에 넣어두고

export const getServerSideProps = wrapper.getServerSideProps(
  async (context) => {
    const parsedCookie = context.req
      ? cookie.parse(context.req.headers.cookie || "")
      : "";
    if (context.req && parsedCookie) {
      if (parsedCookie["accessToken"]) {
        context.store.dispatch({
          type: LOAD_MY_INFO_REQUEST,
          data: parsedCookie["accessToken"],
        });
      } else if(parseCookies["refreshToken"]) {
                 context.store.dispatch({
                    type: NEW_TOKEN_REQUEST,
          data: parsedCookie["refreshToken"],
                }
    }
    context.store.dispatch(END);
    await context.store.sagaTask.toPromise();
  }
);

saga 코드들을 살펴보면

function newTokenAPI(data) {
  return axios.get("/auth/refresh-token", {
    headers: {
      "x-refresh-token": data,
    },
  });
}

function* refreshToken(action) {
  try {
    const result = yield call(newTokenAPI, action.data);
    const { accessToken, refreshToken, info } = result.data.data;
    yield put(
      newhTokenSuccess({ accessToken, refreshToken, info, setCookie })
    );
    cookie.save("accessToken", accessToken, {
      path: "/",
    });
    cookie.save("refreshToken", refreshToken, {
      path: "/",
    });
  } catch (err) {
    yield put(newTokenFailure());
  }
}

이런 코드를 사용하여 새로운 토큰을 받아오고 처리하면 된다.

reducer같은 경우엔 refreshToken을 이용하여 새 토큰을 받아오지 못하는 경우(NEW_TOKEN_FAILURE)에 로그아웃을 시켜줘야한다. NEW_TOKEN_SUCCESS에 경우 받아온 나의 정보들을 스토어에 넣어주면 된다.

두 포스트를 통해 next로 jwt를 ssr하는 방법을 알아보았다.

jwt를 이용한 방식으로 SSR하는 건 복잡하다. 하지만 브라우저에서 관리하는 토큰으로는 ssr이 안되기 때문에 프론트 서버에 저장된 쿠키를 이용하여 요청을 보낸다고 생각하면

getServerSideProps에서 토큰을 집어낸다음 서버로 요청할 때 헤더에 토큰을 붙여준다.

이 개념으로 생각하면 CSR을 한번만 더 풀어내는 방식과 같으므로 CSR과 유사하게 문제를 해결할 수 있다.

해당 코드로 만들어진 프로젝트 GITHUB

댓글