[Toy Project 기록하기 8] 로그인 2. 전역 상태 똑똑하게 공유하기 with Context API
지난번 jwt와 refresh token, access token을 이용해 로그인을 구현했습니다. 그러나 로그인 후 새로고침을 하면 리액트가 다시 렌더링 되면서 로그인 상태가 유지되지 않는 문제가 발생했습니다. 이를 해결하기 위해 로그인한 유저 정보를 전역적으로 저장하고 전달하는 역할이 필요해 보입니다. 리액트에서 이를 효율적으로 구현할 방법을 찾아보고 반영해 보겠습니다.
일반적으로 전역 상태 관리 도구하면 redux를 많이 떠올리실 텐데요. 저는 이번 프로젝트에서 Context API를 이용해 전역 상태를 관리했습니다. 엄밀히 말하자면 전역 상태를 관리했다기보다 값을 저장하고 공유할 수 있게 해주었습니다. 오늘은 제가 redux가 아닌 Context API를 쓴 이유와 개념, 구현 방법에 대해 적어보려고 합니다.
Context API
context
context는 리액트 컴포넌트 간에 데이터를 공유할 수 있게 해주는 기능으로, context를 사용하면 단계마다 일일이 props를 넘겨주지 않고도 컴포넌트 트리 전체에 데이터를 제공할 수 있습니다.
context를 사용하는 경우
context는 앱의 모든 컴포넌트에서 사용할 수 있는 데이터를 전달할 때 유용합니다.
특히 테마(다크 모드/라이트 모드), 인증된 사용자에 관한 데이터, 로케일(지역, 언어) 등 자주 업데이트할 필요가 없는 데이터는 context에 저장하는 것이 좋습니다.
context는 전체적인 상태를 관리하는 목적이 아니라 데이터를 쉽게 공유하기 위해 만들어졌기 때문입니다.
제 프로젝트에서는 복잡한 전역 상태 관리 없이 로그인한 유저 정보를 필요한 컴포넌트에서만 가져다 쓰는 것이 목표였기 때문에 redux 대신 Context API를 활용했습니다.
context 사용법 + useContext 훅
createContext를 통해 context 객체를 생성합니다.
// UserContext.tsx
// createContext(initialValue)
export const UserContext = createContext<TUserContext>({
user: undefined,
isLoading: true,
removeUser: () => {},
getUser: () => {},
});
// ...
생성된 context를 가지고 context provider로 컴포넌트 트리를 감쌉니다. 공유하고자 하는 값을 value props로 설정합니다.
// UserContext.tsx
// ...
export const UserProvider = ({ children }: TUserProvider) => {
const { data, isLoading, remove, refetch } = useQuery(
'myProfile',
getMyProfile
);
const handleRemoveUser = () => {
remove();
refetch();
};
return (
<UserContext.Provider
value={{
user: data?.data,
isLoading,
removeUser: handleRemoveUser,
getUser: refetch,
}}
>
{children}
</UserContext.Provider>
);
};
// index.tsx
import { UserProvider } from './context/UserContext';
// ...
root.render(
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<UserProvider>
<App />
</UserProvider>
</BrowserRouter>
</QueryClientProvider>
);
원하는 컴포넌트에서 useContext를 통해 context에 넣은 값에 바로 접근할 수 있습니다.
// Nav.tsx
import { useContext, useState } from 'react';
import { Link, NavLink } from 'react-router-dom';
import { APP } from '../constances/routes';
import { UserContext } from '../context/UserContext';
import { logoutUser } from '../services/user';
import { onLogoutSuccess } from '../utils/axios';
export default function Nav() {
const { user, removeUser } = useContext(UserContext);
const { mutate: logoutMutation } = useMutation(logoutUser, {
onSuccess: () => {
onLogoutSuccess();
// 로그아웃 성공 시 로그인 유저 정보 지우기
removeUser();
navigate(APP.HOME);
},
});
const logout = () => {
logoutMutation();
};
// ...
return (
<div>
<nav>
<div>
<NavLink to="/">홈</NavLink>
</div>
// user 로그인의 여부에 따라 보여주는 메뉴가 다름
{user ? (
<div>
<button
type="button"
onClick={() => setIsMenuOpen(!isMenuOpen)}
>
마이페이지
</button>
{isMenuOpen && (
<div>
<div>
<Link to="/my-profile">내 프로필</Link>
<Link to="/upload">업로드</Link>
<button type="button" onClick={logout}>
로그아웃
</button>
</div>
</div>
)}
</div>
) : (
<div>
<NavLink to="/login">Login</NavLink>
</div>
)}
</nav>
</div>
);
}
context 사용 시 주의사항
이 프로젝트에서는 전역 상태 관리 없이 단순히 데이터를 전달하기 때문에 redux 대신 context를 적합하다고 판단했습니다. 그렇다면 context가 내려주는 값을 업데이트하는 것은 왜 권장되지 않을까요?
context에서 상태 관리가 필요한 경우 useReducer와 같은 훅과 결합해 서드파티 라이브러리 없이 임시 상태 관리 라이브러리를 생성하는 것이 가능하지만, 보통 성능상의 문제로 권장되지 않습니다. 그 이유는 리액트 context가 재렌더링을 초래하기 때문입니다.
context provider에 저장된 데이터가 업데이트되면 해당 데이터를 사용하는 컴포넌트에서 모두 재렌더링이 일어나게 됩니다.
이러한 경우 state를 더욱 편하고 효율적으로 관리하기 위해 redux나 MobX, Recoil 등 전역 상태 관리 라이브러리를 사용하는 것이 좋습니다.
🧗♀️ 제가 진행한 프로젝트가 궁금하다면
🔽 프론트엔드는 이곳에서 확인하실 수 있습니다.
https://github.com/Team-Madstone/doljabee-fe
GitHub - Team-Madstone/doljabee-fe: Climbing Community
Climbing Community. Contribute to Team-Madstone/doljabee-fe development by creating an account on GitHub.
github.com
🔽 백엔드는 이곳에서 확인하실 수 있습니다.
https://github.com/Team-Madstone/doljabee-be
GitHub - Team-Madstone/doljabee-be: Climbing Community
Climbing Community. Contribute to Team-Madstone/doljabee-be development by creating an account on GitHub.
github.com
참고 자료
https://ko.reactjs.org/docs/context.html