들어가는 말
저는 보통 useContext 를 사용하지 않고, 해당 훅을 내부적으로 사용하고 있는 전역 상태 라이브러리를 사용해서 쓰고 있었습니다. 과거에 다크모드 테마를 전역적으로 관리하기 위한 목적으로 useContext 를 사용한 후에는 굳이 쓸 필요성을 느끼지 못해서 눈 여겨보지는 않았죠.
오늘은 이렇게 서먹해져버린 이 친구에 대해서 다시 상기해보면서 유저의 로그인 상태를 관리하는 간단한 예제를 바탕으로 다시 친해지는 시간을 가져볼까 합니다.
useContext 에 대해 깊이 있고, 명확한 정보는 리액트 공식 문서(https://ko.react.dev/learn/passing-data-deeply-with-context) 를 참고하면 됩니다. 해당 포스트는 간략하게 알아보는 것이 목적이므로 나름의 생각과 이유를 기반으로 글을 정리해보겠습니다.
useContext 가 무엇이고, 왜 사용할까요?
사실 useContext 가 필요로 하는 몇 가지 이유를 정리하면 그 자체가 해당 훅의 개념이라 할 수 있으므로 이에 대해 총 3가지 이유를 정리해보았습니다. 사용되는 예제가 로그인 상태이므로 이를 기반으로 각 이유를 언급하겠습니다.
전역 상태 관리
로그인 상태는 앱 전반에 걸쳐 필요합니다. 사용자가 로그인했는지 여부에 따라 다른 화면을 보여주거나 특정 기능에 접근할 수 있도록 제어해야 합니다. useContext를 사용하면 전역에서 이 상태를 쉽게 공유할 수 있습니다.
Prop Drilling 방지
useContext를 사용하면 중간 컴포넌트를 거치지 않고 로그인 상태를 직접적으로 필요로 하는 컴포넌트에 전달할 수 있습니다. 이는 코드의 복잡성을 줄여주고 유지보수를 쉽게 합니다.
아래는 리액트 공식문서에서 프롭 드릴링의 예시를 보여주고 있습니다.
참고로, 루트 컴포넌트에서 시작된 상태가 리프 컴포넌트 까지 전달이 되고, 해당 상태를 루트 컴포넌트에서 반영하기 위해서 또 다시 상태를 끌어올려야 하는 문제가 발생하는데, 이를 Prop Driling 이라고 합니다. 이게 문제가 되는 이유는 루트 컴포넌트와 리프 컴포넌트 사이에 무수히 많은 prop 전달을 위한 중간 컴포넌트가 존재하고, 해당 prop 을 사용하지 않음에도 불구하고, 모든 중간 컴포넌트가 상태의 변경에 의해 리렌더링되는 문제를 경험할 수 있습니다.
이는 결국 애플리케이션의 성능 저하를 일으키므로 프롭 드릴링 문제가 심각한 경우에는 화면이 버벅이는 문제를 경험할수도 있습니다.
즉, useContext 는 이러한 중간 계층을 거치지 않고 즉시 상태를 사용하는 컴포넌트로 배달하는 역할을 하기 때문에, 성능 저하를 방지할 수 있습니다.
재사용성
useContext를 사용한 로그인 관리 코드는 재사용성이 높습니다. 어디에서든 useContext를 호출하여 로그인 상태에 접근할 수 있습니다. 다시말해, 로그인 유무에 따른 로직의 분기처리가 필요한 곳에서 상태를 재사용할 수 있으니 재사용성이 높아지는 것은 당연한 말입니다.
참고로 예시는 리액트 네이티브 코드로 작성되어 있습니다. 최근에 공부 중이라 적용시켜 보았습니다. |
사용 예시, 로그인 상태를 관리해보자
들어가기 전, 사용되는 예시는 아주 단순한 예시이므로 실제 코드 사용에서 적용하는 것은 부수적인 처리가 필요할 수 있습니다.
AuthContext 생성하기
우선, useContext 를 사용하기 위해서는 Context 를 만들어주어야 합니다. 또한, 이를 전역적으로 사용하기 위한 전역적인 Provider 컴포넌트를 생성하고, children 객체를 해당 프로바이더에 바인딩 함으로써 모든 자식 컴포넌트가 프로바이더의 영향권에 들어오도록 설정해줍니다.
import React, { createContext, useContext, useState } from 'react';
// Context 생성
const AuthContext = createContext();
// Provider 컴포넌트
export const AuthProvider = ({ children }) => {
const [isAuthenticated, setIsAuthenticated] = useState(false);
// 로그인 요청 함수
const login = () => {
setIsAuthenticated(true);
};
// 로그아웃 요청 함수
const logout = () => {
setIsAuthenticated(false);
};
// 여기서 프로바이더의 value 으로 전달된 상태는 children의 자리에 남겨지는 모든 컴포넌트가 사용할 수 있습니다.
return (
<AuthContext.Provider value={{ isAuthenticated, login, logout }}>
{children}
</AuthContext.Provider>
);
};
// useContext 로 반환받은 값을 쉽게 읽어와 사용할 수 있도록 커스텀 훅을 만들어주었습니다.
export const useAuth = () => {
return useContext(AuthContext); // useContext 의 자리에는 {isAuth..., login, logout} 이 남습니다.
};
- AuthContext: 로그인 상태를 관리하기 위한 Context입니다.
- AuthProvider: 로그인 상태와 로그인/로그아웃 기능을 자식 컴포넌트에 제공할 수 있습니다.
- useAuth: useContext를 사용하여 로그인 상태와 관련된 함수들을 간편하게 사용할 수 있도록 하는 커스텀 훅입니다.
로그인 상태 사용하기
앞서 useAuth 으로 useContext 를 사용하기 쉽게 만든 커스텀 훅을 상태관리의 타겟이 되는 컴포넌트에 가져와서 사용할 수 있습니다. useContext 는 인자로 전달 받은 AuthContext 로 부터 provider 의 value 에 할당했던 값을 반환하며, 여기서는 그 값이 {isAuthenticated, login, logout] 이 됩니다.
import React from 'react';
import { View, Text, Button } from 'react-native';
import { useAuth } from './AuthContext'; // 앞에서 만든 AuthContext
const HomeScreen = () => {
const { isAuthenticated, login, logout } = useAuth();
return (
<View>
{isAuthenticated ? (
<>
<Text>Welcome, you are logged in!</Text>
<Button title="Logout" onPress={logout} />
</>
) : (
<>
<Text>You are not logged in.</Text>
<Button title="Login" onPress={login} />
</>
)}
</View>
);
};
export default HomeScreen;
- isAuthenticated: 사용자가 로그인했는지 여부를 나타냅니다.
- login/logout: 사용자가 로그인하거나 로그아웃할 때 호출되는 함수입니다.
- HomeScreen: 로그인 상태에 따라 다른 UI를 렌더링하는 컴포넌트입니다.
프로바이더 감싸기
마지막으로 AuthContext 를 실제 적용할 컴포넌트를 AuthProvider 로 감싸줍니다. 이렇게 되면 앞서 설명한 방식대로 privder 의 value 프로퍼티로 전달한 값을 useContext 로 부터 반환받고, 이 값을 전역적으로 사용할 수 있게 됩니다.
import React from 'react';
import { AuthProvider } from './AuthContext';
import HomeScreen from './HomeScreen';
const App = () => {
return (
<AuthProvider>
<HomeScreen />
</AuthProvider>
);
};
export default App;
AuthProvider: 이 컴포넌트가 앱 전체를 감싸고 있으므로, useAuth를 사용하여 로그인 상태와 관련된 데이터에 접근할 수 있습니다.
[나가는 말] useContext 사용 시 고려할 점
이번에 아주 간단한 예시를 통해 useContext 를 알아보았습니다. 활용하기에 따라서는 전역상태 라이브러리를 사용하지 않고도 충분히 해당 훅을 통해 상태관리가 가능할 것으로 보이지만, useContext 를 사용하는 모든 컴포넌트에서 값의 변동이 발생하면 모든 구독된 컴포넌트 또한 리렌더링이 발생할 수 있기 때문에, 조심할 필요가 있어 보입니다.
또한, 상태가 복잡할수록 상태 분리가 생각보다 어려울 수 있는 생각도 듭니다. 즉, 여러 상태를 관리하는 context 가 산재하는 경우에는 각 상태를 모듈화하여 사용하기에 사실상 관리가 복잡해질 수 있다고 봅니다.
특히, 특정한 상태에 대해서 식별하고 이를 구독하는 방식으로 업데이트, 조회되는 방식이 아니기 때문에 특정 상태를 변경하는 것임에도 불구하고, 관련없는 다른 상태 또한 같이 업데이트될 수 있다는 제한점은 복잡한 상태관리를 요구하는 경우에는 사용성이 제한될 수 있다는 생각도 듭니다.
그럼에도 별도 라이브러리 설치없이 내장되어 있는 훅이므로 그에 따른 유지보수와 활용성은 무궁무진하다고 생각되므로 조금씩 이를 활용할 수 있는 방법들을 찾아서 적용해보면 좋을 것 같습니다.