이 글은 직접 프로젝트에서 적용한 react와 next를 사용한 프로젝트에서 jwt를 이용하여 사용자 정보를 서버사이드 렌더링 하는 것을 다룬다.
대게 웹에서 jwt를 다룰 때 로컬스토리지나 세션 스토리지를 사용하곤 한다.
하지만 ssr을 이용하게 되면 프론트 서버는 로컬스토리지가 없어 사용자의 정보를 가져올 수가 없다.
그렇기에 나의 경우는 쿠키로부터 토큰을 관리하는 방식을 사용했다.
토큰을 관리하는 과정을 정리하면
- 로그인하면 액세스 토큰과 리프레쉬 토큰을 받는다.
- 쿠키와 리덕스에 두 토큰을 넣어놓는다.
- 헤더에 액세스 토큰을 붙여놓는다.
- 새로고침 시 프론트 서버에 있는 쿠키를 바탕으로 내 정보를 불러온다.
리덕스 사가를 이용한 로그인 코드이다
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,
});
}
}
코드를 설명하면 로그인 요청이 되면 시작되는 코드인데
- 성공 시 데이터를 리덕스 스토어에 넣는다
- 앞으로 서버로 정보를 요청할 때 기본적으로 헤더에 access-token이 들어가게끔 한다.
- 쿠키에 토큰을 저장한다.
이걸 이용해서 서버 사이드 렌더링을 하는 코드를 살펴보면
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하는 과정은
- context.req.headers에 쿠키가 있는 지 확인한다.
- 액세스 토큰이 있다면 이를 이용해 정보를 불러온다
- 액세스 토큰이 없고 리프레쉬 토큰만 있다면 토큰도 재발급 받고, 사용자 정보를 재요청한다.
- 받아온 정보들을 리덕스와 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과 유사하게 문제를 해결할 수 있다.
댓글