2023.02.06 Context - API
이제 리액트에서 제공하는 컨텍스트 API를 통해 컨텍스트를 어떻게 사용하는지에 대해서 알아보도록 하겠습니다.
컨텍스트를 사용하기 위해서 가장 먼저 해야 할 일은 컨텍스트를 생성하는 것입니다.
컨텍스트를 생성하기 위해서 React.createContext() 함수를 사용합니다.
아래 코드처럼 함수의 파라미터로 기본값을 넣어 주면 됩니다.
그러고 나면 컨텍스트 객체가 만들어 집니다.
const MyContext = React.createContext(defaultValue);
리액트에서 렌터링이 일어날 때 컨텍스트 객체를 구독하는 하위 컴포넌트가 나오면 현재 컨텍스트의 값을 가장 가까이에 있는 상위 레벨의 Provider로부터 받아오게 됩니다.
그런데 만약 상위 레벨에 매칭되는 Provider가 없다면, 이경우에만 기본값이 사용됩니다.
그렇기 때문에 기본값은 Provider 없이 컴포넌트를 테스트할 때 유용합니다.
참고로 기본값으로 undefined를 넣으면 기본값이 사용되지 않습니다.
React.createContext() 함수를 사용해서 컨텍스트를 만들었다면
이제 하위 컴포넌트들이 해당 컨텍스트의 데이터를 받을 수 있도록 설정해줘야 합니다.
이를 위해서 사용하는 것이 바로 Provider입니다.
데이터를 제공해주는 컴포넌트라고 이해하면 됩니다.
모든 컨텍스트 객체는 Provider라는 리액트 컴포넌트를 갖고 있습니다.
Context.Provider 컴포넌트로 하위 컴포넌트들을 감싸주면 모든 하위 컴포넌트들이
해당 컨텍스트의 데이터에 접근할 수 있게 됩니다.
Provider는 다음 코드처럼 사용하면 됩니다.
<MyContext.Provider value={/* 어떤 값 */}>
Provider 컴포넌트에는 vlaue라는 prop이 있으며,
이것은 provider 컴포넌트 하위에 있는 컴포넌트들에게 전달됩니다.
그리고 하위 컴포넌트들이 이 값을 사용하게 되는데
하위 컴포넌트가 데이터를 소비한다는 의미를 가지고 있어 consumer 컴포넌트라고 부릅니다.
consumer 컴포넌트는 컨텍스트 값의 변화를 지켜보다가 만약 값이 변경되면 재렌더링됩니다.
참고로 하나의 Provider 컴포넌트는 여러 개의 consumer 컴포넌트와
연결될 수 있으며 여러 개의 Provider 컴포넌트는 중첩되어 사용될 수 있습니다.
Provider 컴포넌트로 감싸진 모든 consumer 컴포넌트는 Provider의 value prop이 바뀔때마다 재렌더링됩니다. 값이 변경되었을 때 상위 컴포넌트가 업데이트 대상이 아니더라도 하위에 있는 컴포넌트가 컨텍스트를 사용한다면 하위 컴포넌트에서는 업데이트가 일어납니다.
이때 값의 변화를 판단하는 기준은 자바스크립트 객체의 Object.is라는 함수와 같은 방식으로 판단합니다.
컨텍스트는 재렌더링 여부를 결정할 때 레퍼런스 정보를 사용하기 때문에
Provider의 부모 컴포넌트가 재렌더링되었을 경우, 의도치 않게 consumer 컴포넌트의 재렌더링이
일어날 수 있습니다.
예를 들어 아래 코드는 Provider 컴포넌트가 재렌더링될 때마다 모든 하위 consumer 컴포넌트의 재렌더링이 발생합니다.
왜냐하면 value prop을 위한 새로운 객체가 매번 새롭게 생성되기 때문입니다.
function App(props) {
return (
<MyContext.Provider value={{ something: 'something'}}>
<Toolbar />
</MyContext.Provider>
);
}
이를 방지하기 위해서는 value를 직접 넣는 것이 아니라 컴포넌트의 state로 옮기고
해당 state의 값을 넣어 주어야 합니다.
아래 코드는 수정한 이후의 모습입니다.
import { useState } from "react";
function App(props) {
const [value, setValue] = useState({something: 'something'});
return (
<MyContext.Provider value={value}>
<Toolbar />
</MyContext.Provider>
);
}
Class.contextType은 Provider 하위에 있는 클래스 컴포넌트에서 컨텍스트의 데이터에 접근하기 위해 사용하는 것입니다.
클래스 컴포넌트는 현재 거의 사용하지 않기 때문에 이런 방법이 있다는 정도로만 참고하기 바랍니다.
class MyClass extends React.Component {
componentDidMount() {
let value = this.context;
/* MyContext의 값을 이용하여 원하는 작업을 수행 가능 */
}
componentDidUpdate() {
let value = this.context;
/* ... */
}
componentWillUnmount() {
let value = this.context;
/* ... */
}
render() {
let value = this.context;
/* MyContext의 값에 따라서 컴포넌트들을 렌더링 */
}
}
MyClass.contextType = MyContext;
React.createContext()로 생성한 Context 객체를 원하는 클래스의 contextType 프로퍼티로 지정할 수 있습니다. 이 프로퍼티를 활용해 클래스 안에서 this.context를 이용해 해당 Context의 가장 가까운 Provider를 찾아 그 값을 읽을 수 있게됩니다. 이 값은 render를 포함한 모든 컴포넌트 생명주기 매서드에서 사용할 수 있습니다.
참고로 이 API를 사용하면 단 하나의 컨텍스트만을 구독할 수 있습니다.
여러 개의 컨텍스트를 동시에 사용하는 방법에 대해서는 뒤에서 다루도록 하겠습니다.
consumer 컴포넌트는 앞에서 설명한 것처럼 컨텍스트의 데이터를 구독하는 컴포넌트입니다.
클래스 컴포넌트에서는 위에 나온 Class.contextType을 사용하면 되고
함수 컴포넌트에서는 Context.Consumer를 사용하여 컨텍스트를 구독할 수 있습니다.
<MyContext.Consumer>
{value => /* context의 값에 따라서 컴포넌트들을 렌더링 */}
</MyContext.Consumer>
컴포넌트의 자식으로 함수가 올 수 있는데 이것을 function as a child라고 부릅니다.
Context.Consumer로 감싸주면 자식으로 들어간 함수가 현재 컨텍스트의 value를 받아서
리액트 노드로 리턴하게 됩니다.
이때 함수로 전달되는 value는 Provider의 value prop과 동일합니다.
만약 상위 컴포넌트에 Provider가 없다면 이 value 파라미터는 createcontext()를 호출할 때 넣는 기본값과 동일한 역할을 합니다.
function as a child는 컴포넌트의 자식으로 함수를 사용하는 방법입니다.
리액트에서는 기본적으로 하위 컴포넌트들을 children이라는 prop으로 전달해 주는데 children으로 컴포넌트 대신 함수를 사용하여 아래와 같이 사용할 수 있습니다.
// children이라는 prop을 직접 선언하는 방식
<Profile children={name => <p>이름: {name}</p>} />
// Profile컴포넌트로 감싸서 children으로 만드는 방식
<Profile>{name => <p>이름: {name}</p>}</Profile>
컨텍스트 객체는 displayName이라는 문자열 속성을 가집니다.
또한 크롬의 리액트 개발자 도구에서는 컨텍스트의 Provider나 Consumer를 표시할 때 이 displayName을 함께 표시해 줍니다.
예를 들어 아래와 같이 코드를 작성하면 MyDisplayName이 리액트 개발자 도구에 표시됩니다.
const MyContext = React.createContext(/* some value */);
MyContext.displayName = 'MyDisplayName';
// 개발자 도구에 "MyDisplayName.Provider"로 표시됨
<MyContext.Provider>
// "MyDisplayName.Consumer" in DevTools
<MyContext.Consumer>
클래스 컴포넌트에서 Class.contextType을 사용하면 한 번에 하나의 컨텍스트만 사용할 수 있다고 배웠습니다.
여러 개의 컨텍스트를 동시에 사용하려면 어떻게 해야 할까요?
바로 Context.Provider를 중첩해서 사용하는 방식으로 구현할 수 있습니다.
// 기본값이 light인 ThemeContext
const ThemeContext = React.createContext('light');
// 로그인한 유저 정보를 담는 UserContext
const UserContext = React.createContext({
name: 'Guest',
});
class App extends React.Component {
render() {
const {signedInUser, theme} = this.props;
// context 초기값을 제공하는 App 컴포넌트
return (
<ThemeContext.Provider value={theme}>
<UserContext.Provider value={signedInUser}>
<Layout />
</UserContext.Provider>
</ThemeContext.Provider>
);
}
}
function Layout() {
return (
<div>
<Sidebar />
<Content />
</div>
);
}
// 컨텍스트 컴포넌트는 두 개의 컨텍스트로부터 값을 가져와서 렌더링함
function Content() {
return (
<ThemeContext.Consumer>
{theme => (
<UserContext.Consumer>
{user => (
<ProfilePage user={user} theme={theme} />
)}
</UserContext.Consumer>
)}
</ThemeContext.Consumer>
);
}
둘 이상의 context 값이 함께 쓰이는 경우가 많다면 그 값들을 한 번에 받는 render prop 컴포넌트를 만드는 것을 고려해보세요.
지금까지 클래스 컴포넌트에서 컨텍스트를 사용하는 방법과
함수 컴포넌트에서 Provider와 Consumer를 사용해서 컨텍스트를 사용하는 방법에 대해 배웠습니다.
앞에서 말한 것처럼 클래스 컴포넌트는 이제 거의 사용하지 않기 때문에 함수 컴포넌트에서 컨텍스트를 사용하는 방법을 이해하고 있는 것이 더 중요합니다.
그런데 함수 컴포넌트에서는 컨텍스트를 사용하기 위해 컴포넌트를 매번 Consumer 컴포넌트로 감싸주는 것보다 더 좋은 방법이 있습니다.
바로 훅(Hook)입니다.
useContext()훅은 함수 컴포넌트에서 컨텍스트를 쉽게 사용할 수 있게 해줍니다.
useContext()훅은 React.createContext()함수 호출로 생성된 컨텍스트 객체를 인자로 받아서 현재 컨텍스트의 값을 리턴합니다.
useContext() 훅은 아래와 같이 사용합니다.
function MyComponent(props) {
const value = useContext(MyContext);
return(
...
)
}
useContext() 훅을 사용하면 다른 방식과 동일하게 컴포넌트 트리상에서 가장 가까운 상위 Provider로부터 컨텍스트의 값을 받아오게 됩니다.
만약 컨텍스트의 값이 변경되면 변경된 값과 함께 useContext() 훅을 사용하는 컴포넌트가 재렌더링됩니다. 그렇기 때문에 만약 useContext()훅을 사용하는 컴포넌트의 렌더링이 꽤 무거운 작업일 경우에는 별도로 최적화 작업을 해줄 필요가 있습니다.
또한 useContext() 훅을 사용할 때에는 파라미터로 컨텍스트 객체를 넣어줘야 한다는 것을 꼭 기억하기 바랍니다.
아래 코드처럼 Consumer나 Provider를 넣으면 안됩니다.
useContext(MyContext);
useContext(MyContext.Consumer)
useContext(MyContext.Provider)
import React from "react";
const ThemeContext = React.createContext();
ThemeContext.displayName = "Themecontext";
export default ThemeContext;
여기에서는 컨텍스트의 초깃값을 별도로 설정하지 않았고,
이후 Provider에서 값을 설정할 예정입니다.
그리고 개발자 도구를 통해 컨텍스트의 이름을 확인하기 위해서 ThemeContext의 displayName 값을 설정해 줬습니다.
import { useContext } from "react";
import ThemeContext from "./ThemeContext";
function MainContent(props) {
const { theme, toggleTheme} = useContext(ThemeContext);
return (
<div
style={{
width: "100vw",
height: "100vh",
padding: "1.5rem",
backgroundColor: theme == "light" ? "white" : "black",
color: theme == "light" ? "black" : "white"
}}
>
<p>안녕하세요, 테마 변경이 가능한 웹사이트 입니다.</p>
<button onClick={toggleTheme}>테마 변경</button>
</div>
);
}
export default MainContent;
MainContent 컴포넌트는 ThemeContext로부터 현재 설정된 테마 값을 받아와 실제 화면의 콘텐츠를 렌더링하는 역할을 합니다.
또한 테마 변경 버튼을 누를 경우 ThemeContext로부터 받은 toggleTheme() 함수를 호출하여 ThemeContext의 값을 변경하는 역할도 합니다.
여기에서는 ThemeContext의 값을 가져오기 위해 ThemeContext.Consumer 컴포넌트를 사용하는 방법 대신에 useContext() 훅을 사용했습니다.
import { useState, useCallback } from "react";
import ThemeContext from "./ThemeContext";
import MainContent from "./MainContent";
function DarkOrLight(props) {
const [ theme, setTheme] = useState("light");
const toggleTheme = useCallback(() => {
if (theme == "light"){
setTheme("dark");
} else if (theme == "dark") {
setTheme("light");
}
}, [theme]);
return (
<ThemeContext.Provider value={{ theme, toggleTheme}}>
<MainContent />
</ThemeContext.Provider>
);
}
export default DarkOrLight;
DarkOrLight 컴포넌트는 방금 전에 만든 MainContent 컴포넌트를 자식으로 갖고 있는데 이를 ThemeContext.Provider로 감싸서 ThemeContext의 값을 하위 컴포넌트들이 사용할 수 있도록 해줍니다.
만약 ThemeContext.Provider로 감싸주지 않으면 하위 컴포넌트들이 ThemeContext의 값을 가져올 수 없겠죠?
그리고 ThemeContext의 값으로 들어가는 theme와 toggleTheme() 함수는 자체적으로 관리하고 있습니다.
수정
import React, { Profiler } from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import DarkOrLight from './chapter_14/DarkOrLight';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<DarkOrLight />
</React.StrictMode>
);
reportWebVitals();
버튼을 누르면 테마가 바뀌는 것을 확인할 수 있다.
조금 더 자세히 살펴보기 위해 개발자 도구를 열어 컴포넌트 탭을 클릭해 봅시다.
그러고 나면 다음 그림과 같이 아까 설정한 ThemeContext.displayName이 표시되는 것을 볼 수 있고, 오른쪽에서 ThemeContext의 값도 확인이 가능합니다.
컴포넌트들 사이에서 데이터를 props를 통해 전달하는 것이 아닌 컴포넌트 트리를 동해 곧바로 데이터를 전달하는 방식
어떤 컴포넌트든지 컨텍스트에 있는 데이터에 쉽게 접근할 수 있음
여러 컴포넌트에서 계속해서 접근이 일어날 수 있는 데이터들이 있는 경우
Provider의 모든 하위 컴포넌트가 얼마나 깊이 위치해 있는지 관계없이 컨텍스트의 데이터를 읽을 수 있음
컴포넌트와 컨텍스트가 연동되면 재사용성이 떨어짐
다른 레벨의 많은 컴포넌트가 데이터를 필요로 하는 경우가 아니라면,
기존 방식대로 props를 통해 데이터를 전달하는 것이 더 적합
React.createContext( )
Context.Provider
Class.contextType
Context.Consumer
Context.displayName
함수 컴포넌트에서 컨텍스트를 쉽게 사용할 수 있게 해주는 훅
React.createContext( ) 함수 호출로 생성된 컨텍스트 객체를 인자로 받아서 현재 컨텍스트의 값을 리턴
컨텍스트의 값이 변경되면 변경된 값과 함께 useContext( )훅을 사용하는 컴포넌트가 재렌더링됨