더 큰 리액트 앱에서 마주칠 수 있는 문제에 대해 알아보자.
문제: 프롭을 통해 많은 컴포넌트를 거쳐 많은 데이터를 전달할 때 일어나는 문제
일반적으로 데이터는 프롭을 통해 컴포넌트에 전달되는데, state를 여러 컴포넌트를 통해 전달할 경우 문제가 생길 수 있다.
isLoggedIn 데이터가 실제로 필요한 곳은 Navigation 컴포넌트이기 때문에 이런식으로 아래로아래로 전달하고 있다. 그러면 중간에 prop을 단지 전달만 하는 컴포넌트들이 생긴다. 즉, 데이터를 필요로 하는 다른 컴포넌트에 직접적으로 연결되지 않는다. 앱이 커진다면 전달하는 경로가 점점 더 길어질 수 있다.
따라서 데이터를 컴포넌트를 통해 다른 컴포넌트에 전달하려는 큰 프롭 체인이 만들어 질 수 있다. 이런 현상이 딱히 나쁜 건 아니지만 앱이 커질수록 불편해진다.
prop을 실제로 필요한 데이터를 부모로부터 받는 컴포넌트에서만 사용할 수 있게 하면 훨씬 간편하다. 부모가 데이터를 관리하지도 필요하지도 않으므로 부모를 통해 데이터를 전달하지 않도록 말이다.
이를 위해 컴포넌트 전체에서 사용할 수 있는, 리액트에 내장된 내부 state 저장소가 있는데 바로 React Context이다. 이를 사용하면, 긴 프롭 체인을 만들지 않고도, 컴포넌트 전체 state저장소에서 액션을 트리거하여 관련된 컴포넌트에 직접 전달할 수 있다.
리액트 내부적으로 state를 관리할 수 있게 해준다.
프롭 체인을 구축하지 않아도, 앱의 어떤 컴포넌트에서도 state를 직접 변경하여, 앱의 다른 컴포넌트에 state를 직접 전달할 수 있게 해준다.
store (또는 state 나 context) 폴더를 추가한다.
파일 이름은 마음대로 하면 되는데, AuthContext.js
처럼 파스칼 표기법으로 표시하면 여기에 컴포넌트를 저장한다는 뜻이 되어버리므로, auth-context.js
같이 소문자 케밥 표기법을 사용하여 이름을 정하자.
앱의 여러개의 전역 state에 대해 여러 개의 컨텍스트를 가질 수 있다. 더 큰 state에 대해 하나의 컨텍스트만 사용할 수도 있고, 그냥 자기 마음이다. 여기서는 일단 컨텍스트가 어떻게 작동되는지 살펴보자.
createContext()
는 기본(default) 컨텍스트를 만든다.
import React from "react";
React.createContext();
컨텍스트는 앱이나 빈 state의 컴포넌트일 뿐이므로 state가 어때야 하는지는 본인이 정하면된다.
앱이나 컴포넌트 수준에서 state는 그냥 텍스트일 수도 있다.
기본 state로 그냥 텍스트를 써도 된다.
React.createContext("my state");
하지만 대부분의 경우 객체이다.
이 경우에는 isLoggedIn의 state를 관리하므로 기본 값으로 { isLoggedIn: false }
를 주면 된다.
React.createContext({
isLoggedIn: false,
});
createContext()
에서 얻는 결과는 컴포넌트 혹은, 컴포넌트를 포함하는 객체가 된다. 따라서 이름을 AuthContext
로 정하자. AuthContext 자체가 컴포넌트는 아니지만, 컴포넌트를 포함할 객체이기 때문이다.
다른 곳에서 이 컨텍스트 객체를 사용해야 하기 때문에 export default로 내보낸다.
import React from "react";
const AuthContext = React.createContext({
isLoggedIn: false,
});
export default AuthContext;
공급한다는 것은 JSX 코드로 감싸는 것을 뜻하는데, 감싸는 모든 컴포넌트는 컨텍스트에 접근 권한이 생긴다.
컨텍스트를 활용해야 하는 모든 컴포넌트를 JSX 코드로 감싸면 해당 컨텍스트를 리스닝할 수 있게 된다.
예)
모든 컴포넌트에 isLoggedIn이 필요하기 때문에 <AuthContext.Provider>
로 모든 컴포넌트를 감싸준다. 이 컨텍스트를 wrapper 컴포넌트처럼 활용했기 때문에 기존에 사용하던 <></>
리액트 프래그먼트는 삭제해도 된다.
//...
import AuthContext from "./store/auth-context";
//...
return (
//<>
<AuthContext.Provider>
<MainHeader isAuthenticated={isLoggedIn} onLogout={logoutHandler} />
<main>
{!isLoggedIn && <Login onLogin={loginHandler} />}
{isLoggedIn && <Home onLogout={logoutHandler} />}
</main>
</AuthContext.Provider>
//</>
);
이렇게 하면 MainHeader, Login, Home 등 모든 자식 컴포넌트에서 AuthContext에 접근할 수 있다.
컨텍스트 값에 접근하려면 리스닝해야 한다.
리스닝하는데는 두 가지 방법이 있다.
1. 컨텍스트 소비자<Context.Consumer></Context.Consumer>
로 리스닝하기
2. useContext()
훅 사용하여 리스닝하기
일반적으로는 리액트 훅을 사용하여 리스닝하지만, 소비자로 리스닝 하는 방법도 알아보자.
Navigation 에서 사용자가 인증되었는지 여부를 알고 싶다고 해보자.
이를 위해 AuthContext를 사용할 수 있다.
<AuthContext.Consumer>
//..
</AuthContext.Consumer>
() => {}
여야 한다. (ctx) => {}
를 가져오면 객체{ isLoggedIn: false }
를 얻게된다.props.isLoggedIn
대신 ctx.isLoggedIn
으로 컨텍스트에 접근할 수 있다.import React from "react";
import AuthContext from "../../store/auth-context";
import classes from "./Navigation.module.css";
const Navigation = (props) => {
return (
//✅ AuthContext 소비자로 감싸기
<AuthContext.Consumer>
//✅ 소비자는 함수, 인수로 AuthContext 객체를 보냄
{(ctx) => {
//✅ 함수의 리턴 값으로 Nav의 JSX 반환
return (
<nav className={classes.nav}>
<ul>
//✅ AuthContext 객체 안의 isLoggedIn 값 사용
{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>
);
}}
//✅ AuthContext 소비자로 감싸기
</AuthContext.Consumer>
);
};
export default Navigation;
⚠️ 하지만 위 코드를 저장하면 코드 충돌 오류가 뜬다.
왜냐하면 컨텍스트의 기본값은 실제로, 공급자 없이 소비하는 경우에만 사용되기 때문이다. 엄밀히 말하면, 컨텍스트에 디폴트값이 있으면 공급자는 필요하지 않다.
하지만 이 패턴은 기억해 두자! 변할 수 있는 값을 가지기 위해 컨텍스트를 사용하는데 이는 공급자로만 가능하다.
//...
return (
<AuthContext.Provider value={{ isLoggedIn: false }}>
<MainHeader isAuthenticated={isLoggedIn} onLogout={logoutHandler} />
<main>
{!isLoggedIn && <Login onLogin={loginHandler} />}
{isLoggedIn && <Home onLogout={logoutHandler} />}
</main>
</AuthContext.Provider>
);
}
✅ 충돌을 피하기 위해서는 공급자 컴포넌트에 value 프롭을 추가해야 한다.
내가 만든 컴포넌트가 아니기 때문에 value에 컨텍스트 객체를 전달하면 된다.
<AuthContext.Provider value={{ isLoggedIn: false }}>
그러면 해당 객체를 변경할 수 있게 된다. state나 앱 컴포넌트를 통해서 값이 변경될 때 마다 새로운 state값이 모든 소비 컴포넌트에 전달된다.
하지만 컨텍스트의 데이터를 하드 코딩하여 가져오는 대신 app.js에서 관리하고 있는 isLoggedIn state를 컨텍스트 객체에 전달하여 가져와 보자.
//...
return (
<AuthContext.Provider value={{ isLoggedIn: isLoggedIn }}>
<MainHeader isAuthenticated={isLoggedIn} onLogout={logoutHandler} />
<main>
{!isLoggedIn && <Login onLogin={loginHandler} />}
{isLoggedIn && <Home onLogout={logoutHandler} />}
</main>
</AuthContext.Provider>
);
}
✅ 앱 컴포넌트에서 공급자를 설정할 때, <AuthContext.Provider value={{ isLoggedIn: isLoggedIn }}>
라고 설정하면 된다.
앱 컴포넌트에서 isLoggedIn state를 관리하는데, 공급자가 전달하는 값에 isLoggedIn을 false로 하드 코딩 하는 대신 state값인 isLoggedIn 을 전달하면 value 객체는 isLoggedIn state가 변경될 때마다 리액트에 의해 업데이트 된다. 그리고 그 새로운 컨텍스트 객체는 모든 리스닝 컴포넌트로 전달된다.
//...
return (
<AuthContext.Provider value={{ isLoggedIn: isLoggedIn }}>
<MainHeader onLogout={logoutHandler} />
<main>
{!isLoggedIn && <Login onLogin={loginHandler} />}
{isLoggedIn && <Home onLogout={logoutHandler} />}
</main>
</AuthContext.Provider>
);
}
✅ 이처럼 컨텍스트를 사용하면 state를 전달하기 위해 프롭을 사용할 필요가 없어진다.
대신 공급자에 value 프롭을 설정하면된다. 그러면 모든 자식 컴포넌트에서 이 값을 리스닝할 수 있다.
isLoggedIn을 프롭으로 전달하는 부분들은 삭제하면 된다.
소비자는 컨텍스트를 소비하는 한 가지 방법으로 나름 괜찮은 방법이지만 함수도 있고, 코드도 반환하고.. 좀 복잡스럽다 ^^~ 컨텍스트 훅을 사용하여 리스닝하는 방법을 알아보자.
useContext()
훅은 컨텍스트 사용, 활용, 리스닝 할 수 있게 하는 훅이다.//✅
import React, { useContext } from "react";
const Navigation = (props) => {
//✅
useContext();
return (
//...
useContext()
훅에 사용하려는 컨텍스트를 가리키는 포인터를 전달하면 컨텍스트 값을 얻을 수 있다.import React, { useContext } from "react";
import AuthContext from "../../store/auth-context";
const Navigation = (props) => {
//✅
useContext(AuthContext);
return (
//...
import React, { useContext } from "react";
import AuthContext from "../../store/auth-context";
import classes from "./Navigation.module.css";
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은 동적으로 전달하고 있지만 여전히 로그인 핸들러/로그아웃 핸들러는 컴포넌트의 prop으로 전달하고 있다. 컴포넌트 뿐만 아니라 함수에도 데이터를 전달하기 위해 동적 컨텍스트를 설정할 수 있다.
//MainHeader로는 context를 전달> 그 아래 컴포넌트인 Navigation 컴포넌트로 컨텍스트 전달
<AuthContext.Provider
value={{
isLoggedIn: isLoggedIn,
//함수도 전달가능
onLogout: logoutHandler,
}}
>
<MainHeader />
<main>
{!isLoggedIn && <Login onLogin={loginHandler} />}
{isLoggedIn && <Home onLogout={logoutHandler} />}
</main>
</AuthContext.Provider>
//Login과 Home 컴포넌트로는 props으로 전달
<MainHeader />
로 context를 전달하여 그 아래 컴포넌트인 <Navigation />
컴포넌트로 컨텍스트 전달하여 데이터를 사용하고 있다. 이처럼 바로 아래 컴포넌트가 데이터를 사용하지 않고 전달만 하는 경우에는 컨텍스트를 사용하면 좋다.
<Login />
, <Home />
컴포넌트로는 props을 통해 핸들러를 보내고 있다. 두 컴포넌트는 props을 직접 참조하기 때문에 컨텍스트 보다 props을 사용하는 것이 좋다.
props을 사용하면 좋은 경우
대부분의 경우 props을 사용하여 컴포넌트에 데이터를 전달한다.
바로 아래 컴포넌트에서 직접 props을 사용할 경우, 컨텍스트 보다 props을 사용하는 것이 좋다.
프롭은 컴포넌틀르 구성하고 그것들을 재사용할 수 있도록 하는 매커니즘을 가졌기 때문이다.
<Login />
,<Home />
컴포넌트에서는 데이터를 직접 사용하고 있음<Home />
컴포넌트에서는 <Home />
컴포넌트에 있는 <Button />
컴포넌트에 propsonLogout
으로 logoutHandler
를 전달하고 있다. <Button />
같은 프리젠테이션을 위한 컴포넌트의 경우, 버튼 클릭을 항상 onLogout에 바인딩하기 위해 버튼 내부에 굳이 컨텍스트를 사용하지는 않는다. 그렇게 하면 onLogout을 전달할 필요는 없어지지만 또한, 버튼이 클릭될 때마다 사용자를 로그아웃시키는 것 외의 다른 일을 할 수 없게 되기 때문이다. 따라서 props을 사용해도 충분하다.컨텍스트를 사용하면 좋은 경우
앱 구조나 데이터를 관리하는 방법이나 선호에 따라 <App />
컴포넌트에 있는 많은 로직을 하나의 파일로 옮겨서 깔끔하게 관리하고 싶을 수도 있다. 별도의 state 및 컨텍스트 관리 컴포넌트를 만들고 싶은 경우, 아래 처럼 파일 하나에 몰빵하면 집중적으로 분리된 접근 방식을 사용할 수 있다.
이렇게 하나의 파일에 몰빵할 경우 하나의 중앙 저장소는 기존의 <App />
컴포넌트가 아닌 전용 컨텍스트 컴포넌트(<AuthContext />
), 전용 컨텍스트 파일(/src/store/auth-context.js
)이 된다.
import React, { useState, useEffect } from "react";
//기본값
const AuthContext = React.createContext({
isLoggedIn: false,
//직접 사용 하지는 않지만 IDE 자동 완성 더 좋게 하기 위해 더미 함수 저장해 두기
onLogout: () => {},
//인자에 뭐 들어가는지도 작성해 두면 IDE 자동 완성에 좋음
onLogin: (email, password) => {},
});
//✅ 컴포넌트로 공급자 컴포넌트를 만들 수 있음
//✅ 기본 값이 아닌, 명명 값으로 기본 값에 추가하여 AuthContextProvider 도 내보낸다.
export const AuthContextProvider = (props) => {
//✅ App 컴포넌트에 있던 로직 다 여기로 옮기기
//🔥 공급자 컴포넌트 안에서 isLoggedIn state와 setState 관리
const [isLoggedIn, setIsLoggedIn] = useState();
//🔥 useEffect도 여기로
// 로그인 버튼 클릭하지 않아도 즉 loginHandler가 트리거 되지 않아도, 로컬스토리지에 isLoggedIn의 값이 1인 경우에는 로그인 유지하기
useEffect(() => {
const storedUserLoggedInInformation = localStorage.getItem("isLoggedIn");
if (storedUserLoggedInInformation === "1") {
setIsLoggedIn(true);
}
}, []);
//디펜던시가 비어 있는 경우, 의존성이 없기 때문에 앱이 처음 시작되면 의존성이 변경된 것으로 간주되어 앱이 시작될 때 이펙트 코드는 딱 한 번만 실행됨
//🔥 로그인 핸들러
const loginHandler = (email, password) => {
// We should of course check email and password
// But it's just a dummy/ demo anyways
localStorage.setItem("isLoggedIn", "1");
setIsLoggedIn(true);
};
//🔥 로그아웃 핸들러
const logoutHandler = () => {
localStorage.removeItem("isLoggedIn");
setIsLoggedIn(false);
};
//🔥이렇게 전체 인증 state를 이 별도의 공급자 컴포넌트에서 관리할 수 있음
//✅ 반환하는 공급자 컴포넌트 <AuthContext.Provider>의 value로 값 넣어 주기
return (
<AuthContext.Provider
value={{
isLoggedIn: isLoggedIn,
onLogout: logoutHandler,
onLogin: loginHandler,
}}
>
{props.children}
</AuthContext.Provider>
);
};
//이렇게하면 <AuthContextProvider /> 컴포넌트에서 전체 로그인 state를 관리하고, 모든 컨텍스트를 설정할 수 있다.
//기본값 내보내기
export default AuthContext;
이렇게 기존에 <App />
컴포넌트에 있던 모든 로직을 컨텍스트 파일로 옮긴다.
이제 <AuthContextProvider />
컴포넌트로 index.js에서 App을 렌더링 하는 부분을 감싸주자.
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
//AuthContextProvider 컴포넌트는 명명값으로 가져옴
import { AuthContextProvider } from "./store/auth-context";
import App from "./App";
const root = ReactDOM.createRoot(document.getElementById("root"));
//AuthContextProvider 컴포넌트로 앱을 감싸서 앱을 렌더링한다.
//이렇게 하면 모든 앱에서 컨텍스트를 가져다 쓸 수 있다.
root.render(
<AuthContextProvider>
<App />
</AuthContextProvider>
);
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() {
// ✅ useContext() 사용하여 AuthContext 가져오기
const ctx = useContext(AuthContext);
return (
//이제 하나의 저장소에서 관리하기 때문에 props으로 전달하던거 다 없애고 모두 컨텍스트로..
<>
<MainHeader />
<main>
{!ctx.isLoggedIn && <Login />}
{ctx.isLoggedIn && <Home />}
</main>
</>
);
}
export default App;
로그인 컴포넌트와 홈 컴포넌트에서도 useContext
에 컨텍스트 파일을 임포트 해서 AuthContext를 포인팅하여 컨텍스트를 사용하게 하면 된다.
<App />
컴포넌트는 간결해졌다.💬 이렇게 중앙집중적으로 관리하는 것은 완전히 선택사항이다.
여튼 이렇게 하는거~ 저렇게 하는거~ 패턴 익혀 두면 좋으니 알아두자.
현재 컨텍스트를 사용하여 사용자 클릭 시 항상 로그아웃되게 하고 있는데, 그렇게 하면 사용자를 로그아웃 시키는 것 외에 다른 용도로 재사용할 수 없다.
이런 경우 버튼 컴포넌트에서는 컨텍스트를 사용하면 안된다. 오히려 prop을 사용하여 버튼을 구성하는 것이 좋다.
🌟 구성하려면 prop 사용하고, 컴포넌트 또는 전체 앱에서 state 관리하려면 context 사용하는 것이 좋다.
🌀 그런데 이렇게해도 한계가 있다.
위와 같은 경우, 컨텍스트를 사용하고 싶은데 프롭은 또 적당하지 않은 것 같을 때는 어떻게 해야 할까?
그럴 때 쓰라고 사람들이 만들어둔게 있다. 그럴 땐 리덕스를 사용해보자! 😇
props은 여전히 컴포넌트 구성에서 필수적이고 중요하다.
긴~ props 체인을 교체하기 위해서 컨텍스트를 사용하는 것은 좋은 대안이지만, 모든 컴포넌트의 커뮤니케이션을 대체하기 위해 context를 사용하지는 말자~!