Context
- 컴포넌트들 사이에서 데이터를 props로 전달하는 것이 아닌 컴포넌트 트리를 통해 곧바로 데이터를 전달하는 방식
- 어떤 컴포넌트든지 context에 있는 데이터에 쉽게 접근할 수 있음
- props로 상위 컴포넌트에서 하위 컴포넌트로 데이터를 전달하면 여러 컴포넌트에 걸쳐서 자주 사용되는 데이터(로그인 여부, 프로필 정보 등)를 전달하려면 반복적인 코드가 많이 생기고 지저분해짐
- 예를 들어 아래 그림에서 루트 노드에 있는 데이터를 C 컴포넌트로 전달하려면 최소 2번 props로 전달해야 함
- 컨텍스트를 사용하면 일일이 props로 전달할 필요 없이 아래 그림처럼 데이터를 필요로 하는 컴포넌트에 곧바로 데이터를 전달할 수 있음
context를 사용하는 경우
- 여러 컴포넌트에서 계속해서 접근이 일어날 수 있는 데이터들이 있는 경우
- Provider의 모든 하위 컴포넌트가 얼마나 깊이 위치해 있는지 관계없이 컨텍스트의 데이터를 읽을 수 있음
예제 코드로 이해해보자
- 현재 선택된 테마를 기존 방식대로 컴포넌트의 props로 전달하는 코드
- 상위 컴포넌트인 App 컴포넌트에서 Toolbar 컴포넌트로 theme라는 이름의 prop으로 현재 테마인 dark를 넘김
- Toolbar 컴포넌트에서는 ThemeButton 컴포넌트를 사용하는데 ThemeButton 컴포넌트에서 현재 테마를 필요로 함
- 따라서 prop으로 전달받은 theme을 하위 컴포넌트인 ThemeButton 컴포넌트에 전달
function App(props) {
return <Toolbar theme="dark" />
}
function Toolbar(props) {
return (
<div>
<ThemeButton theme={props.theme} />
</div>
);
}
function ThemeButton(props) {
return <Button theme={props.theme} />;
}
- 위와 같은 방식은 실제 데이터를 필요로 하는 컴포넌트까지의 깊이가 깊어질수록 복잡해짐
- context를 사용하면 이러한 방식을 개선할 수 있음
const ThemeContext = React.createContext('light');
function App(props) {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar(props) {
return (
<div>
<ThemeButton />
</div>
);
}
function ThemeButton(props) {
return (
<ThemeContext.Consumer>
{value => <Button theme={value}/>}
</ThemeContext.Consumer>
);
}
- 위 코드에서는 먼저
React.createContext()
함수를 사용해서 ThemeContext라는 이름의 컨텍스트 생성
- 컨텍스트를 사용할 컴포넌트의 상위 컴포넌트에서 Provider로 감싸주어야 하는데 여기에서는 최상위 컴포넌트인 App 컴포넌트를
ThemeContext.Provider
로 감싸줌
- 이렇게 하면 Provider의 모든 하위 컴포넌트가 얼마나 깊이 위치해 있는지 관계 없이 컨텍스트의 데이터를 읽을 수 있음
context 사용 전 고려해야할 점
- 컴포넌트와 컨텍스트가 연동되면 재사용성이 떨어짐
- 다른 레벨의 많은 컴포넌트가 데이터를 필요로 하는 경우가 아니라면, 기존 방식대로 props를 통해 데이터를 전달하는 것이 더 적합
Context API
React.createContext
- 컨텍스트를 생성하기 위한 함수
- 컨텍스트 객체를 리턴함
const MyContext = React.createContext(기본값);
- 리액트에서 렌더링이 일어날 때 컨텍스트 객체를 구독하는 하위 컴포넌트가 나오면 현재 컨텍스트의 값을 가장 가까이에 있는 상위 레벨의 Provider로부터 받아움
- 만약 상위 리벨에 매칭되는 Provider가 없다면 기본값 사용
- 기본값으로 undefined를 넣으면 기본값이 사용되지 않음
Context.Provider
- 모든 컨텍스트 객체는 Provider라는 컴포넌트를 갖고 있음
- Provider 컴포넌트로 하위 컴포넌트들을 감싸주면 모든 하위 컴포넌트들이 해당 컨텍스트의 데이터에 접근할 수 있게 됨
<MyContext.Provider value={}>
- Provider에는 value라는 prop이 있으며, 이것이 데이터로써 하위에 있는 컴포넌트들에게 전달됨
- 하위 컴포넌트는 데이터를 소비한다는 의미로 consumer 컴포넌트라고 부름
- consumer 컴포넌트는 컨텍스트 값의 변화를 지켜보다가 값이 변경되면 재렌더링됨
- 하나의 Provider 컴포넌트는 여러 개의 consumer 컴포넌트와 연결될 수 있으며 여러 개의 Provider 컴포넌트는 중첩되어 사용될 수 있음
- 주의 사항
- 컨텍스트는 재렌더링 여부를 결정할 때 레퍼런스 정보를 사용하기 때문에 Provider의 부모 컴포넌트가 재렌더링 되었을 경우 의도치 않게 consumer 컴포넌트의 재렌더링이 일어날 수 있음
- 이를 방지하기 위해서는 value를 직접 넣어주는 것이 아니라 컴포넌트의 state로 옮기고 해당 state 값을 넣어주어야 함
function App(props) {
return (
<MyContext.Provider value={{something: 'something'}}>
<Toolbar />
</MyContext.Provider>
)
}
function App(props) {
const [value, setValue] = useState({ something: 'something' });
return (
<MyContext.Provider value={value}>
<Toolbar />
</MyContext.Provider>
)
}
Class.contextType
- Provider 하위에 있는 클래스 컴포넌트에서 컨텍스트의 데이터에 접근하기 위해 사용
- 단 하나의 컨텍스트만을 구독할 수 있음
- 아래 코드 처럼
MyClass.contextType = MyContext;
라고 해주면 MyClass 컴포넌트는 MyContext 데이터에 접근 가능
class MyClass extends React.Component {
componentDidMount() {
let value = this.context;
}
componentDidUpdate() {
let value = this.context;
}
componentWillUnmount() {
let value = this.context;
}
render() {
let value = this.context;
}
}
MyClass.contextType = MyContext;
Context.Consumer
- 컨텍스트의 데이터를 구독하는 컴포넌트
- 데이터를 소비한다는 뜻에서 consumer 컴포넌트라고도 부름
<ThemeContext.Consumer>
{value => >}
</ThemeContext.Consumer>
- consumer 컴포넌트는 컨텍스트 값의 변화를 지켜보다가 값이 변경되면 재렌더링됨
- 하나의 Provider 컴포넌트는 여러 개의 consumer 컴포넌트와 연결될 수 있음
- 상위 레벨에 매칭되는 Provider가 없을 경우 기본값이 사용
function as a child
- 컴포넌트의 자식으로 함수를 사용하는 방법
- 리액트에서는 기본적으로 하위 컴포넌트들을 children이라는 prop으로 전달해주는데 children으로 컴포넌트 대신 함수를 사용하여 아래와 같이 사용
<Profile children={name => <p>이름 : {name}</p>} />
<Profile>{name => <p>이름: {name}</p>}</Profile>
Context.displayName
- 컨텍스트 객체는 displayName이라는 문자열 속성을 가짐
- 크롬의 리액트 개발자 도구에서는 컨텍스트의 Provider나 Consumer를 표시할 때 이 displayName을 함께 표시해줌
- 예를 들어 아래와 같이 코드를 작성하면 MyDisplayName이 리액트 개발자 도구에 표시됨
const MyContext = React.createContext();
MyContext.displayName = 'MyDisplayName';
<MyContext.Provider>
<MyContext.Consumer>
여러 개의 context 사용하기
- Provider 컴포넌트와 Consumer 컴포넌트를 여러 개 중첩해서 사용하면 됨
예제 코드
const ThemeContext = React.createContext('light');
const UserContext = React.createContext({
name: 'Guest',
});
class App extends React.Component {
render() {
const { signedInUser, theme } = this.props;
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>
);
}
- 위 코드에서는 App 컴포넌트에서는 ThemeContext와 UserContext 두 개의 컨텍스트에 대해 두 개의 Provider를 사용하여 자식 컴포넌트인 Layout을 감싸줌
- 실제 사용하는 Content 컴포넌트에서 두 개의 Consumer 컴포넌트를 사용하여 데이터 전달
- 이렇게 하면 여러 개의 컨텍스트를 동시에 사용 가능
- 하지만 두 개 또는 그 이상의 컨텍스트의 값이 자주 함께 사용될 경우 모든 값을 한 번에 제공해주는 별도의 render prop 컴포넌트를 직접 만드는 것이 더 좋음
useContext()
- 함수 컴포넌트에서 컨텍스트를 쉽게 사용할 수 있게 해주는 훅
React.createContext()
함수 호출로 생성된 컨텍스트 객체를 인자로 받아서 현재 컨텍스트의 값을 리턴
- 컨텍스트의 값이 변경되면 변경된 값과 함께
useContext()
훅을 사용하는 컴포넌트가 재렌더링됨
function MyComponent(props){
const value = useContext(MyContext);
return (
...
)
}
useContext()
훅을 사용할 때에는 파라미터로 컨텍스트 객체를 넣어줘야 함
- Consumer나 Provider를 넣으면 안됨!
useContext(MyContext);
실습 - 컨텍스트를 사용하여 테마 변경 기능 만들기
- 컨텍스트를 사용하여 테마를 변경할 수 있는 기능을 만들어보자!
컨텍스트 만들기
- 초기값을 별도로 설정하지 않고, 개발자 도구를 통해 컨텍스트 이름을 확인하기 위해
ThemeContext
의 displayName
값을 설정해준다.
import React from "react";
const ThemeContext = React.createContext();
ThemeContext.displayName = "ThemeContext";
export default ThemeContext;
자식 컴포넌트 만들기
ThemeContext
로부터 현재 설정된 테마 값을 받아와 실제 화면의 콘텐츠를 렌더링하는 컴포넌트
- 테마 변경 버튼을 누를 경우
ThemeContext
로부터 받은 toggleTheme()
함수를 호출하여 ThemeContext의 값을 변경하는 역할도 함
- 여기서는
ThemeContext.Consumer
이 아닌 useContext()
훅을 사용하여 ThemeContext
를 받아옴
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.Provider
로 감싸 ThemeContext
값을 하위 컴포넌트들이 사용할 수 있도록 함
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;
실행 결과
- 처음화면은 흰색 배경에 검은 글씨인 light 테마로 나타나다가 테마변경 버튼을 누르면 아래 그림과 같이 검은색 배경에 흰색 글씨인 dark 테마로 바뀌는 것을 볼 수 있음
- 개발자 도구를 확인해보면 위에서 설정한
ThemeContext.displayName
이 표시되는 것을 볼 수 있음
실습 전체 코드
References