AS-IS
현재 사용자의 상태 관리
useUserState라는 리코일 훅에서 전역으로 사용자의 정보를 관리하고 있다.
// useUserState.ts
import { recoilPersist } from 'recoil-persist';
const { persistAtom } = recoilPersist();
const userAtom = atom<RegisterResponse | null>({
key: 'user',
default: null,
effects_UNSTABLE: [persistAtom],
});
export default function useUserState() {
const getUserAtom = useRecoilValue(userAtom); // 전역 상태를 불러오는 훅
const setUserAtom = useSetRecoilState(userAtom); // 전역 상태를 업데이트하는 훅
const isLogin = getUserAtom !== null;
const userData = getUserAtom;
const token = getUserAtom?.token;
const setUser = (data: RegisterResponse) => {
setUserAtom(data);
console.log(data.email);
};
const clearUser = () => setUserAtom(null);
return { isLogin, userData, token, setUser, clearUser };
}
새로고침 시 데이터를 보존하기 위해 recoil-persist를 사용하고 있다.
: recoil의 상태값을 Storage와 동기화해주는 라이브러리
그리고, 로그인 후 리다이렉트되는 페이지에서 권한 서버로부터 전달받은 code 값을 백엔드 서버에 전송하여 AccessToken을 전달받는다. 이후, 이 AccessToken을 localStorage에 저장하는 방식이다.
// auth/page.tsx
export default function Page() {
const code = useSearchParams().get('code');
const { setUser } = useUserState();
const router = useRouter();
useEffect(() => {
if (code) {
getRegister(code).then((response) => {
setUser(response);
localStorage.setItem('accesstoken', response.token.accessToken);
alert(`로그인에 성공했어요!`);
router.back();
});
}
});
return;
}
localStorage에서 사용자 정보를 받아오고, 데이터를 클라이언트에서 받아오게 된다. 하지만 서버에서 넘어오는 문서에는 데이터가 없다.
현재 방식의 문제점
서버 상태와 클라이언트 상태를 분리하기 위해 react-query와 recoil을 사용하고 있는데, 이 둘이 현재 혼재되어 있다. 사용자의 정보는 서버로부터 받아온 데이터이기 때문에 서버 상태이다. 만약 저장을 해야 한다면, useQuery의 onSuccess 함수 내부에서 localStorage나 cookie 등 브라우저 저장소에 저장하면 된다.
TO-BE
- react-query를 통해 사용자의 정보를 받아오고, 새로고침 이전까지는 캐싱된 데이터를 활용한다.
- 여기서 세 가지 방안이 있다.
- onSuccess에서 localStorage에 저장한다.
- onSuccess에서 cookie에 저장한다.
- 따로 브라우저에 저장하지 않는다.
a안은, Next.js 서버 단에서 접근하지 못한다.
b안은, Next.js 서버 단에서 접근할 수 있어 SSR 구현이 가능하다. 만약 브라우저 Cookie에 값이 저장되어 있다면, 서버 단에서 따로 호출할 필요가 없다. 그러나 브라우저 Cookie에 접근해야 하므로 SSG 구현은 불가능하다.
c안도 괜찮은 방법이다. 새로고침하면 페이지 데이터를 새로 불러오듯, 사용자 데이터를 다시 불러오는 것도 괜찮다고 생각한다.
결론
우선, 현재 구현 방식은 사용자의 정보를 localStorage에 저장하는 것이다. localStorage는 서버에서 접근할 수 없으니 cookie로 저장 위치를 변경하고, 백엔드 서버로부터 AccessToken과 사용자 정보를 전달받으면 cookie에 저장하도록 하자.
그리고, Access Token을 로컬 스토리지나 쿠키에 직접 저장하는 것은 보안에 상당히 취약하다. 특히 XSS(JavaScript를 이용한 정보 탈취)에 취약하다. 백엔드 서버에 httponly 쿠키 설정을 할 수 있는지 물어보자.
- 로그인 시, 서버로부터 전달받은 Access Token을 우선 쿠키에 저장한다.
- 추후, 백엔드 팀과 이야기하여, httpOnly 옵션으로 서버 측에서 브라우저 쿠키에 저장할 수 있는지 물어본다.
- 사용자 정보는 쿠키에 저장한다.
- 사용자 정보에 접근할 때 쿠키에서 데이터를 불러온다.
- 로그인 페이지에 접속 시, middleware에서 쿠키를 통해 사용자 정보가 있는지 확인하여 없다면 로그인 페이지로 리다이렉트한다.
개발
1. 현재 사용자 정보 불러오는 훅을 어디서 사용하는 지 확인한다.
이 페이지 혹은 컴포넌트들이 이 훅을 사용하는 곳들이다. 우리가 추후 교체해야 할 페이지 혹은 컴포넌트이다.
2. cookie를 이용한 사용자 정보 관리 로직을 추가한다.
// apps/auth/components/authComponent.tsx
try {
const {
token: { accessToken },
email,
nickname,
} = await getRegister(code);
setClientCookie(ACCESS_TOKEN, accessToken);
setClientCookie(EMAIL, email);
setClientCookie(NICKNAME, nickname);
alert(`로그인에 성공했어요!`);
router.push('/');
} catch (error) {
router.push('/');
}
토큰과 사용자 정보를 서버로부터 받아오는 auth페이지에서, 쿠키에 토큰 설정과 함께 email, nickname을 설정한다.
// components/Login/LoginSection.tsx
export default function LoginSection() {
// ..
const email = getClientCookie(EMAIL);
const nickname = getClientCookie(NICKNAME);
// ..
}
PR
다음 스탭
이제, 사용자 정보를 localStroage 및 recoil에서 cookie로 마이그레이션을 했으니, SSR와 RSC 사용에 더 용이해졌다. 다음 스텝은 RSC와 RCC를 분리하면 될 듯하다.
트러블 슈팅
AS-IS
- PR이 겹친다. 다른 프론트엔드 팀원은 이틀 후에야 리뷰를 해줄 수 있다고 한다. 그런데, 다른 PR(회원가입 페이지 컴포넌트 리팩토링)에서 auth 관련 로직을 건드렸다. 이번 브랜치에서 사용자 정보를 쿠키로 옮기며 두 수정 사항이 겹친다. 이런 경우에는 어떻게 해야 할까?
- 팀원이 리뷰하고, 해당 브랜치가 머지될 때까지 기다린다.
- 브랜치를 해당 PR의 브랜치로부터 분기한다.
- 1번은 프로젝트 진행의 지연을 야기한다. 두 PR은 애초에 서로 연관이 있는 PR이니, 기존 브랜치에서 파도록 하자.
TO-BE
- 새로운 브랜치를 판다.
git checkout -b 브랜치명
- Cherry Pick
-
git cherry-pick A^..BA커밋부터 B커밋까지 체리픽을 한다.
-