context API는 리엑트에서 제공하는 내장된 기능으로 전역 상태를 관리하기 위한 기능을 제공하는 리액트 공식 API이다.
컴포넌트 트리의 깊은 곳에 위치한 컴포넌트들도 해당 데이터에 접근이 가능하여 해당 상태를 필요로하는 컴포넌트에서 직접적으로 사용할 수 있어서 불필요한 props drilling을 방지할 수 있다는 이점이 있다.
전역 상태 관리 : 여러 컴포넌트에서 공유해야 하는 상태를 관리할 때 Context API를 사용할 수 있다.
예를들어, 사용자 인증정보, 언어 설정, 테마설정 등과 같은 전역적으로 사용되는 데이터를 관리할 수 있다.
컴포넌트 간 통신 : Context API를 사용하면 컴포넌트 간에 데이터를 전달하기 위해 props를 여러단계로 거치지 않고 간편하게 전달할 수 있다.
이는 컴포넌트간 결합도를 낮추고 코드의 가독성도 좋아진다.
테마 변경 : Context API를 사용하여 앱의 테마를 동적으로 변경할 수 있다. 사용자가 테마를 변경할 때마다 해당 변경 내용을 context를 통해 앱 전체에 전파할 수 있게된다.
권한 관리 : 사용자 인증과 같은 권한 정보를 관리할 때 Context API를 사용할 수 있다. 인증된 사용자 정보를 context에 저장해서 앱 내에서 필요한 컴포넌트들이 해당 정보에 접근도록 할 수 있다.
그럼 어떻게 사용하는걸까?
우선 React 객체에서 React.createContext 속성을 사용해서 컨텍스트 객체를 생성해준다.
import React from "react";
const AuthContext = React.createContext({
isLoggedIn: false,
});
export default AuthContext;
import React, { useState, useEffect } from "react";
import Login from "./components/Login/Login";
import Home from "./components/Home/Home";
import MainHeader from "./components/MainHeader/MainHeader";
import AuthContext from "./store/auth-context";
function App() {
const [isLoggedIn, setIsLoggedIn] = useState(false);
useEffect(() => {
const userInfo = localStorage.getItem("isLoggedIn");
if (userInfo === "1") {
setIsLoggedIn(true);
}
}, []);
const loginHandler = (email, password) => {
localStorage.setItem("isLoggedIn", "1");
setIsLoggedIn(true);
};
const logoutHandler = () => {
localStorage.removeItem("isLoggedIn");
setIsLoggedIn(false);
};
return (
// provider가 없이 consumer하는 경우에만 기본값이 사용되고,
하지만 실제론 변할 수 있는 값을 가질것이기 때문에 필요하며 이땐 값 props를 추가해 준다. (value prop)
// provider 에 전달할 value prop을 설정하면 나머지 자식 컴포넌트에 전달 하고있던 같은 상태들은 다 지워줘도 된다.
<AuthContext.Provider value={{ isLoggedIn : isLoggedIn}}>
<MainHeader onLogout={logoutHandler} />
<main>
{!isLoggedIn && <Login onLogin={loginHandler} />}
{isLoggedIn && <Home onLogout={logoutHandler} />}
</main>
</AuthContext.Provider>
);
}
export default App;
createContext 인자값으론 보통은 객체가 온다.
creatContext에서 얻는 결과는 컴포넌트 자체이거나 컴포넌트를 포함하는 객체가 될것이다.
리액트에 해당 context를 앱에서 활용할것이기 때문에 연동 시키는 작업을 해야한다.
여기서 작업이란 해당 컨텍스트를 공급(Provider)도 해주고 소비(Consumer)도 해줘야하는 작업을 말한다.
공급한다는 것은 jsx코드로 감싸는 것을 뜻한다. 감쌀땐 컨텍스트에서 관리하는 상태를 필요로하는 컴포넌트들을 모두 감싸주고 그에 해당하는 컨텍스트를 공급을 해주는것이기 때문에 context객체의 속성인 'Provider'를 사용한다. 그렇게하면 해당 컨텍스트 공급자는 컴포넌트가 된다.
이처럼 감싸게 된다면 해당 컴포넌트로 감싸게 된 모든 자식과 자식의 자식, 그 밑에 까지 전부 해당 컨텍스트에 접근 가능하다.
또한,
value 프롭으로 전달된 상태가 변경될 때마다 리액트에 의해 업데이트 되고, 새로운 컨텍스트 객체는 모든 리스닝 컴포넌트로 전달 된다.
그 다음 접근을 하기 위해선 '리스닝'을 해줘야한다. 2가지 방법으로 할 수 있다.
- context.Consumer 사용
- 리엑트 훅 사용 (가장 많이 사용됨)
1. context.Consumer 사용
사용자가 로그인 인증여부를 알게 하기위해 해당 데이터가 필요한 모든것을 context.Consumer로 감싸주면 된다.
import React from "react";
import classes from "./Navigation.module.css";
import AuthContext from "../../store/auth-context";
const Navigation = (props) => {
return (
// context.Consumer는 Provider와는 다르게 자식을 가지고 함수형태여야한다.
// 중괄호 사이에 함수를 넣고 인자로 컨텍스트의 데이터객체를 가져오고, 해당 jsx코드를 반환한다.
<AuthContext.Consumer>
{(ctx) => {
return (
<nav className={classes.nav}>
<ul>
{ctx.isLoggedIn && (
<li>
<a href="/">Users</a>
</li>
)}
{ctx.isLoggedIn && (
<li>
<a href="/">Admin</a>
</li>
)}
{ctx.isLoggedIn && (
<li>
<button onClick={props.onLogout}>Logout</button>
</li>
)}
</ul>
</nav>
);
}}
</AuthContext.Consumer>
);
};
export default Navigation;
이렇게 Consumer로 리스닝 할 수도 있지만,
컨텍스트 훅을 사용하는 편이 더 깔끔하고 가독성이 좋다.
2. 리엑트 훅 사용(useContext)
먼저 React 객체에서 useContext를 import하고,
사용할 컴포넌트 함수내에서 useContext를 호출하면 된다.
그리고 컨텍스트에게 포인터(정의한 컨텍스트객체)를 전달해준다.
그렇게 되면 해당 컨텍스트 값을 얻을 수 있게된다.
import React, {useContext} from "react";
import classes from "./Navigation.module.css";
import AuthContext from "../../store/auth-context";
const Navigation = (props) => {
const ctx = useContext(AuthContext);
return (
<nav className={classes.nav}>
<ul>
{ctx.isLoggedIn && (
<li>
<a href="/">Users</a>
</li>
)}
{ctx.isLoggedIn && (
<li>
<a href="/">Admin</a>
</li>
)}
{ctx.isLoggedIn && (
<li>
<button onClick={props.onLogout}>Logout</button>
</li>
)}
</ul>
</nav>
);
};
export default Navigation;
훨씬 간편하다!
이제 다른 상태와 함수도 적용해줄 수 있을 것이다.
isLoggedIn 상태를 컨텍스트를 이용해줬지만,
로그아웃 헨들러도 같은 방법으로 해줄 수 있다.
App.js
import React, { useState, useEffect } from "react";
import Login from "./components/Login/Login";
import Home from "./components/Home/Home";
import MainHeader from "./components/MainHeader/MainHeader";
import AuthContext from "./store/auth-context";
function App() {
const [isLoggedIn, setIsLoggedIn] = useState(false);
useEffect(() => {
const userInfo = localStorage.getItem("isLoggedIn");
if (userInfo === "1") {
setIsLoggedIn(true);
}
}, []);
const loginHandler = (email, password) => {
localStorage.setItem("isLoggedIn", "1");
setIsLoggedIn(true);
};
const logoutHandler = () => {
localStorage.removeItem("isLoggedIn");
setIsLoggedIn(false);
};
return (
// 추가로 onLogout 함수도 포인터를 전달해준다.
<AuthContext.Provider
value={{ isLoggedIn: isLoggedIn, onLogout: logoutHandler }}
>
<MainHeader />
<main>
{!isLoggedIn && <Login onLogin={loginHandler} />}
{isLoggedIn && <Home onLogout={logoutHandler} />}
</main>
</AuthContext.Provider>
);
}
export default App;
Navigation.js
import React, {useContext} from "react";
import classes from "./Navigation.module.css";
import AuthContext from "../../store/auth-context";
const Navigation = () => {
const ctx = useContext(AuthContext);
return (
<nav className={classes.nav}>
<ul>
{ctx.isLoggedIn && (
<li>
<a href="/">Users</a>
</li>
)}
{ctx.isLoggedIn && (
<li>
<a href="/">Admin</a>
</li>
)}
{ctx.isLoggedIn && (
<li>
<button onClick={ctx.onLogout}>Logout</button>
</li>
)}
</ul>
</nav>
);
};
export default Navigation;
따라서 언제 props를 사용하고 언제 context를 사용할지 알 수 있다.
대부분의 경우엔 props를 사용해 컴포넌트에 데이터를 전달해준다.
props는 컴포넌트를 구성하고 그것들을 재사용할 수 있도록 하는 메커니즘이기 때문이다.
그래서 많은 컴포넌트를 통해서 체이닝 되는걸 방지하여 전달하고자 하는것이 있을때나, 특정적인 일을 하는 컴포넌트(네비게이션컴포넌트에서 로그아웃 버튼은 단지 로그아웃만 하기위한 버튼이다.)에 사용하는게 좋다.
더 많은 로직을 가져오고 싶을 경우엔, 별도의 컨텍스트 관리 컴포넌트를 추가로 만들어서 컨텍스트를 관리해 줄 수도 있다.
이렇게 되면 App에서 관장하는 상태들을 줄여 간결해지고 오히려 jsx반환하는것과 화면에 무엇을 가져오는데에 더 집중할 수 있게된다.
auth-context.js
import React, { useState, useEffect } from "react";
// 로그인 정보를 관장하는 컨텍스트 객체 생성
const AuthContext = React.createContext({
isLoggedIn: false,
onLogout: () => {},
onLogin: (email, password) => {},
});
// 컨텍스트 객체의 로직을 관리할 컨텍스트 프로바이더 컴포넌트 생성
// 그리고 기존에 App에서 관장하던 상태와 함수들을 전부 여기서 이동시켜 관리해준다.
export const AuthContextProvider = (props) => {
const [isLoggedIn, setIsLoggedIn] = useState(false);
useEffect(() => {
const userInfo = localStorage.getItem("isLoggedIn");
if (userInfo === "1") {
setIsLoggedIn(true);
}
}, []);
const loginHandler = (email, password) => {
localStorage.setItem("isLoggedIn", "1");
setIsLoggedIn(true);
};
const logoutHandler = () => {
localStorage.removeItem("isLoggedIn");
setIsLoggedIn(false);
};
return (
<AuthContext.Provider
value={{
isLoggedIn: isLoggedIn,
onLogout: logoutHandler,
onLogin: loginHandler,
}}
>
{props.children}
</AuthContext.Provider>
);
};
export default AuthContext;
위처럼 App에서 관리해줬던 전역데이터들을 컨텍스트 프로바이더 컴포넌트에 옮겨 작업 시켜주었다.
그 후 이제 기존에 상태 및 함수가 적용되었던 컴포넌드들에 useContext 훅을 불러와서 사용해 주면된다.
먼저 index.js 에서 App컴포넌트를 렌더링 해주는 곳에 AuthContestProvider 컴포넌트로 감싸준다.
index.js
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import { AuthContextProvider } from "./store/auth-context";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<AuthContextProvider>
<App />
</AuthContextProvider>
);
App.js
import React, { useContext } from "react";
import Login from "./components/Login/Login";
import Home from "./components/Home/Home";
import MainHeader from "./components/MainHeader/MainHeader";
import AuthContext from "./store/auth-context";
function App() {
// 컨텍스트 프로바이더가 아닌 객체를 인자로 받는다!
const ctx = useContext(AuthContext);
return (
<>
<MainHeader />
<main>
{!ctx.isLoggedIn && <Login />}
{ctx.isLoggedIn && <Home />}
</main>
</>
);
}
export default App;
Home.js
import React, { useContext } from "react";
import classes from "./Home.module.css";
import Button from "../UI/Button/Button";
import Card from "../UI/Card/Card";
import AuthContext from "../../store/auth-context";
const Home = (props) => {
const authCtx = useContext(AuthContext);
return (
<Card className={classes.home}>
<h1>Welcome back!</h1>
<Button onClick={authCtx.onLogout}>Logout</Button>
</Card>
);
};
export default Home;
Login.js
import React, { useState, useEffect, useReducer, useContext } from "react";
import Card from "../UI/Card/Card";
import classes from "./Login.module.css";
import Button from "../UI/Button/Button";
import AuthContext from "../../store/auth-context";
...(생략)
const Login = (props) => {
const [formIsValid, setFormIsValid] = useState(false);
const [email, emailDispatch] = useReducer(emailReducer, {
value: "",
isValid: null,
});
const [password, passwordDispatch] = useReducer(passwordReducer, {
value: "",
isValid: null,
});
const authCtx = useContext(AuthContext);
// 새로운 폼 전달 함수
const submitHandler = (event) => {
event.preventDefault();
authCtx.onLogin(email.value, password.value);
};
return (
...
)
};
export default Login;
이로써 이렇게 하나의 중앙에 state관리가 있는 코드를 작성하게 되면 컴포넌트들이 각자 하나의 역할만 수행할 수 있도록 갖게 되어 훨씬 더 코드에 집중할 수 있게되는 최적화된 상태를 표현가능해 진다.
위에서 다뤄봤듯이, 여러 컴포넌트에 영향을 미치는 state들에는 적합한 방식으로 동작한다.
하지만 컴포넌트 구성을 대체할 순 없다.
예를 들면 현재 Button 컴포넌트는 버튼 UI를 재사용하기 위해서 만들었다.
이를 사용하는 버튼 요소는 굳이 '로그아웃'기능에만 특화되어 로직이 구성된다면 로그아웃밖에 할 수 없는 버튼 컴포넌트 아니겠는가?
이러한 경우엔 context를 이용하지 않고 props를 사용하여 버튼을 구성하는것이 좋다.
구성을 하려면 props를 사용하고,
컴포넌트 또는 전체 앱에서 state관리를 하려면 context를 사용한다.
하지만, 이러한 경우도 한계는 존재한다.
변경이 잦은 state의 경우엔 context는 적합하지 않다.
예를들어 1초마다 state가 변경되는 업데이트 로직같은 경우다.
리액트 컨텍스트는 이에 최적화 되어있진 않다.
이는 실제로 리액트 개발 공식 사이트에서 언급한 내용이다.
사실 이러한 단점 혹은 한계가 존재했기에
리액트 개발자들이 전역상태를 관리하기위해 사용하는 "리덕스"가 사용되는 기반이 되었던 것이다.