최근 포트폴리오 블로그형식의 프로젝트를 수행했다. 로그인 과정에 발생한 troble shooting을 기록한다.
헤더에 로그인한 계정의 이메일/닉네임을 표시해야한다.
로그인이 성공하면 nickname, email, token을 localstorage에 저장했다. 그리고 토큰이 있는지 여부를 토대로 헤더에 사용자 정보를 표시할려고 했다.
문제1. header.tsx의 localstorage.getItem(key)이 로그인 상태가 바뀐것을 인지하지 못해 초기값(해당하는 key가 없음)으로 인지된다. 따라서 nickname, email이 표시되지 않는다.
문제1을 해결하기 위해서는 로그인 상태에 대한 전역관리가 필요하다.
문제2. 문제1을 해결했다. 그러나 새로고침을 하면 또 다시 localstorage.getItem(key)이 작동하지 못한다. nickname, email이 표시되지 않는다.
문제3. 문제2를 해결했다. 그런데 로그아웃을 한 후 헤더에 nickname, email이 여전히 표시된다. 즉, 동기화가 안되는 문제가 발생한다.
문제2,3 를 해결하기위해 useEffect()로 화면이 렌더링될때마다 로그인 상태값을 다시 가져오게 했다.
header와 main은 형제관계의 컴포넌트다.
서로 로그인상태가 변함을 파악해야 그에 따른 렌더링이 일어나는데 이를 위해서는 로그인과 로그아웃을 포함하여 상위에서 관리해주어야한다.
로그인 상태를 전역관리를 하기 위해서는 recoil, redux와 같은 전역관리를 사용할 수 있지만 이 프로젝트에서는 로그인만 관리하면 되기때문에 '오버스펙이다'라는 생각이 들었다.
따라서 가볍고 간편하게 사용할 수 있는 context API 를 선택했다.
문제1. 해결
const AuthContext = createContext({
isLoggedIn: !!getToken(),
userInfo: {
token: getToken(),
user: {
userId: localStorage.getItem("user_id"),
email: localStorage.getItem("email"),
},
} as UserInfo | undefined,
login: (loginData: UserInfo): void => {},
logout: (): void => {},
});
컨텍스트생성과 초기 상태 정의한다.
isLoggedIn: 사용자가 로그인되었는지 나타내는 boolean 값.
초기값은 getToken 함수를 사용하여 토큰이 있는지 여부를 기반으로 설정된다.
userInfo: 토큰, 사용자 ID, 이메일 및 닉네임과 같은 사용자 정보를 포함하는 객체다. 초기값은 로컬 스토리지에서 가져온 값을 기반으로 설정된다.
login: 사용자 정보를 인수로 받는 로그인 로직을 처리하는 함수. (구현x)
logout: 로그아웃 로직을 처리하는 함수(구현 x)
자식 요소에 컨텍스트를 제공하는 역할을 하는 함수형 컴포넌트다.
const AuthProvider = ({ children }: Props) => {
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [userInfo, setUserInfo] = useState<UserInfo>();
useEffect(() => {
setIsLoggedIn(!!getToken());
setUserInfo({
token: getToken() as string,
user: {
email: localStorage.getItem("email") as string,
userId: localStorage.getItem("user_id") as string,
nickname: localStorage.getItem("nickname") as string,
},
});
}, []);
const login = ({ token, user }: UserInfo) => {
localStorage.setItem("token", token);
localStorage.setItem("user_id", user.userId);
localStorage.setItem("email", user.email);
localStorage.setItem("nickname", user.nickname);
setIsLoggedIn(true);
setUserInfo({ token, user });
};
const logout = () => {
localStorage.removeItem("token");
localStorage.removeItem("user_id");
localStorage.removeItem("email");
localStorage.removeItem("nickname");
setIsLoggedIn(false);
setUserInfo(undefined);
};
return (
<AuthContext.Provider value={{ isLoggedIn, userInfo, login, logout }}>
{children}
</AuthContext.Provider>
);
};
useState hook을 사용하여 isLoggedIn 및 userInfo의 내부 상태를 관리한다.
useEffect hook은 컴포넌트가 마운트된 후 한 번 실행되며, 토큰이 있는지 여부에 따라 isLoggedIn을 업데이트한다. 이메일, 사용자 ID 등을 로컬 스토리지에서 가져온 다음 userInfo로 설정한다.
login 및 logout 함수를 정의한다. 이 함수는 내부 상태를 업데이트하고 사용자 인증과 관련된 추가 로직을 수행한다.
AuthContext.Provider 컴포넌트를 반환한다. 이 컴포넌트는 자식 요소를 감싸고 isLoggedIn, userInfo, login 및 logout 함수를 포함한 현재 컨텍스트 값을 제공한다.
문제2. 해결
useEffect(() => {
setIsLoggedIn(!!getToken());
setUserInfo({
token: getToken() as string,
user: { ... },
});
}, []);
이 부분이 새로고침 시, 다시 유저정보를 가져오는데 실패하던 것을 성공으로 바꾼 코드다. 새로고침으로 화면이 새롭게 마운트되고 렌더링될 때 유저보를 다시 가져오는 역할을 한다.
<AuthProvider>
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/join" element={<Join />} />
<Route path="/login" element={<Login />} />
<Route
path="/user/login/kakao/callback"
element={<KakaoRedirection />}
/>
<Route path="/read/:id" element={<Detail />} />
<Route path="/:userId/projects" element={<UserProjectList />} />
<Route path="/likedList" element={<LikedList />} />
<Route element={<PrivateRoute />}>
<Route path="/edit/:id" element={<Post />} />
<Route path="/post" element={<Post />} />
</Route>
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
</AuthProvider>
전역관리를 위해 AuthProvider로 Route들을 감싸준다.
const useLogin = () => {
const navigate = useNavigate();
const { login } = useContext(AuthContext);
const handleLoginSuccess = (data: LoginResponse) => {
login({
token: data.token,
user: {
userId: String(data.userId),
email: data.email,
nickname: data.nickname,
},
});
};
const { mutate } = useMutation({
mutationFn: loginMutation,
onSuccess: (data: LoginResponse) => {
handleLoginSuccess(data);
showToast({
type: "success",
message: "로그인 성공했습니다.",
});
navigate("/");
},
onError: (error: any) => {
const response = error.response as AxiosResponse;
const code = response.data.code;
const ErrorMessage = getErrorMessage(code);
showToast({ type: "error", message: ErrorMessage });
console.error("Login Error:", error);
},
});
return { mutate };
};
useLogin이라는 커스텀 훅이다.
이때 로그인이 성공적으로 수행되면,
토큰과 유저정보가 localstorage에 저장되고, 또 전역값으로도 관리된다.
const { isLoggedIn, userInfo, logout } = useContext(AuthContext);
const [localIsLoggedIn, setLocalIsLoggedIn] = useState(isLoggedIn);
const [localUser, setLocalUser] = useState<UserInfo>();
const onNavigate = (url: string) => {
navigate(url);
};
const handleLogout = () => {
logout();
showToast({ type: "success", message: "로그아웃 성공하였습니다." });
onNavigate("/");
};
useEffect(() => {
setLocalUser(userInfo);
setLocalIsLoggedIn(isLoggedIn);
}, [isLoggedIn, userInfo]);
문제3. 해결
처음에는 로컬상태 관리를 따로 해주지 않았는데 이때 로그아웃이 되어도 헤더의 정보가 그대로 보이는 문제가 있었다. 즉, 동기화가 안되는 문제가 생긴 것이다.
isLoggedIn과 userInfo 값 (전역) 이 변경될 때마다 로컬 상태를 업데이트한다.
부모 Context의 값이 변경되더라도 로컬 컴포넌트는 바로 업데이트되지 않기 때문에 로컬 상태를 동기화해주는 역할이 필요했다.
그래서 useEffect hook의 의존성을 isLoggedIn, userInfo로 두어 logout 이후 최신 값을 로컬 상태에 반영했다.
즉, useEffect hook을 사용하여 로컬 상태를 AuthContext의 최신 값과 동기화 유지했다.