[React] Context API
Context
context를 이용하면 단계마다 일일이 props를 넘겨주지 않고도 컴포넌트 트리 전체에 데이터를 제공할 수 있다
// App.js
return (
<React.Fragment>
<MainHeader isAuthenticated={isLoggedIn} onLogout={logoutHandler} />
<main>
{!isLoggedIn && <Login onLogin={loginHandler} />}
{isLoggedIn && <Home onLogout={logoutHandler} />}
</main>
</React.Fragment>
);
App 컴포넌트에서 로그인 여부인 isLoggedIn state와 이 상태를 변경하는 함수들은 props을 통해 다른 컴포넌트로 전달된다.
문제가 되는 것은 이것들을 여러 컴포넌트를 거쳐서 전달하는 것이다.
const MainHeader = (props) => {
return (
<header className={classes['main-header']}>
<h1>A Typical Page</h1>
<Navigation isLoggedIn={props.isAuthenticated} onLogout={props.onLogout} />
</header>
);
};
export default MainHeader;
MainHeader 컴포넌트를 보면 App에서 전달한 두 가지 prop중 어떤 것도 사용하고 있지 않고 Navigation 컴포넌트에 다시 전달하는 용도로만 사용하고 있다.
앱의 규모가 커진다면 데이터를 전달하는 경로가 점점 더 길어질 수 있고 prop chain이 만들어지기 쉽다. 앱이 커질수록 불편해진다.
이런 불편함을 없애고자 컴포넌트 전체에서 사용할 수 있는 리액트에 내장된 내부적인 state 저장소, context라는 것이 있다.
이를 통해 긴 prop 체인을 만들지 않고 데이터와 관련된 컴포넌트에 직접 전달할 수 있다.
컴포넌트 구성을 하려면 props을 사용하고, 컴포넌트 또는 전체 앱에서 state 관리를 하려면 context를 사용한다.
위의 로그인 여부 state를 context를 이용해 다른 컴포넌트로 전달한다고 하자.
1. context 만들기 (createContext)
React.createContext(defaultValue);
createContext는 Context 객체를 생성하고, 이 객체는 컴포넌트를 포함할 객체가 된다.
defaultValue는 Provider없이 context를 읽으려할 때 사용된다.
// store/auth-context.js
const AuthContext = React.createContext({
isLoggedIn: false
});
export default AuthContext;
2. Provide
공급한다는 것은 context를 활용할 수 있어야 하는 모든 컴포넌트를 JSX 코드로 감싸는 것을 의미한다.
createContext로 만든 객체는 컴포넌트가 아니라 컴포넌트를 포함할 객체이다. 이 객체의 속성으로 Provider라는 것이 있는데, 이것이 컴포넌트이다.
Provider는 context를 구독하는 컴포넌트들에게 context의 변화를 알리는 역할을 하고, value prop을 받아 하위에 있는 컴포넌트에게 전달한다.
Provider 하위에서 context를 구독하는 모든 컴포넌트는 Provider의 value prop이 바뀔 때마다 다시 렌더링 된다.
// App.js
import AuthContext from './store/auth-context';
return (
<AuthContext.Provider
value={{
isLoggedIn: isLoggedIn,
}}
>
<MainHeader onLogout={logoutHandler} />
<main>
{!isLoggedIn && <Login onLogin={loginHandler} />}
{isLoggedIn && <Home onLogout={logoutHandler} />}
</main>
</AuthContext.Provider>
);
3. Context 접근
3.1 Consumer (잘 사용X)
useContext 훅이 등장하기 전에 context에 접근했던 방법이다.
return (
<AuthContext.Consumer>
{(ctx) => {
return (
<nav className={classes.nav}>
<ul>
{ctx.isLoggedIn && (
<li>
<a href='/'>Admin</a>
</li>
)}
{ctx.isLoggedIn && (
<li>
<button onClick={props.onLogout}>Logout</button>
</li>
)}
</ul>
</nav>
);
}}
</AuthContext.Consumer>
);
Context.Consumer의 자식은 함수이고, 이 함수는 인수로 context의 현재값(Provider의 value prop 값)을 받고 JSX코드(리액트 노드)를 반환해야 한다.
2. useContext
useContext의 인수로 읽으려는 context를 가리키는 포인터를 전달한다. (createContext로 만든 context 객체)
const Navigation = (props) => {
const ctx = useContext(AuthContext);
return (
<nav className={classes.nav}>
<ul>
{ctx.isLoggedIn && (
<li>
<a href='/'>Admin</a>
</li>
)}
{ctx.isLoggedIn && (
<li>
<button onClick={props.onLogout}>Logout</button>
</li>
)}
</ul>
</nav>
);
};
export default Navigation;
context 개선시키기(별도의 context 관리 컴포넌트 만들기 )
createContext에 전달하는 defaultValue로, Provider의 value prop에서 사용할 것들을 넣어준다. 이 기본값은 사용되지 않기 때문에 함수는 더미 함수를 넣어도 된다.
이렇게 기본값을 넣어주면 나중에 context 객체로 접근할 때 자동 완성을 더 쉽게 할 수 있다.
import React, { useEffect, useState } from 'react';
const AuthContext = React.createContext({
isLoggedIn: false,
onLogout: () => {},
onLogin: (email, password) => {},
});
export const AuthContextProvider = (props) => {
const [isLoggedIn, setIsLoggedIn] = useState(false);
useEffect(() => {
const userLoggedIn = localStorage.getItem('isLoggedIn');
if (userLoggedIn === '1') {
setIsLoggedIn(true);
}
}, []);
const logoutHandler = () => {
localStorage.removeItem('isLoggedIn');
setIsLoggedIn(false);
};
const loginHandler = (email, password) => {
localStorage.setItem('isLoggedIn', '1');
setIsLoggedIn(true);
};
return (
<AuthContext.Provider
value={{ isLoggedIn, onLogout: logoutHandler, onLogin: loginHandler }}
>
{props.children}
</AuthContext.Provider>
);
};
export default AuthContext;
이제 auth-context.js 파일에서 context를 관리한다.
AuthContextProvider 컴포넌트를 만들어서 App.js 에서 하던 전체 로그인 state를 관리한다.
// index.js
root.render(
<AuthContextProvider>
<App />
</AuthContextProvider>
);
index.js에서 App 컴포넌트만 렌더하던 것을 AuthContextProvider로 감싸주어 렌더하면 된다
function App() {
const ctx = useContext(AuthContext);
return (
<>
<MainHeader />
<main>
{!ctx.isLoggedIn && <Login />}
{ctx.isLoggedIn && <Home />}
</main>
</>
);
}
별도의 전용 context 파일을 만들어 App 컴포넌트를 더욱 간결하게 만들 수 있고, 이 컴포넌트는 이제 JSX를 반환하는 것과 화면에 렌더하는 것에 집중할 수 있게 된다.