1. 컨텍스트란 무엇인가?
props를 이용한 방식
- 데이터가 컴포넌트의 props를 통해 부모에서 자식으로 단방향 전달
- 여러 컴포넌트에 걸쳐서 자주 사용되는 데이터를 전달하려 하면 반복적인 코드가 많이 생기고 지저분해짐
컨텍스트를 이용한 방법
- 컴포넌트 트리 (component tree)를 통해 곧바로 컴포넌트에 전달
- 컨텍스트를 사용하면 일일이 props로 전달할 필요 없이 데이터를 필요로 하는 컴포넌트에 곧바로 데이터를 전달할 수 있음
- 해당 데이터와 데이터의 변경사항을 모두 하위 컴포넌트들에게 broadcast 해줌
- 컨텍스트를 사용하기에 적합한 데이터의 대표적인 예로 현재 지역정보, UI 테마, 캐싱된 데이터 등이 있음
2. 언제 컨텍스트를 사용해야 할까?
- 여러 컴포넌트에서 자주 필요로 하는 데이터로는 사용자의 로그인 여부, 로그인 정보, UI 테마, 현재 선택된 언어 등이 있음
ex)
- 현재 선택된 테마를 전달
1) 기존의 props을 이용
- 실제 데이터를 필요로 하는 컴포넌트까지의 깊이가 길어질수록 복잡
function App(props) {
return <Toolbar theme="dark" />;
};
function Toolbar(props) {
//Toolbar 컴포넌트는 ThemeButton에 theme를 넘겨주기 위해서 'theme' prop을 가져야만 함
//현재 테마를 알아야 하는 모든 버튼에 대해서 props로 전달하는 것은 굉장히 비효율적
return (
<div>
<ThemeButton theme={props.theme} />
</div>
);
}
function ThemeButton(props) {
return <Button theme={props.theme} />;
}
2) 컨텍스트 사용
- React.createContext() 함수를 사용해서 ThemeContext라는 이름의 컨텍스트를 생성
- 컨텍승트를 사용할 컴포넌트의 상위 컴포넌트에서 Provider로 감싸주어야 하므로 APP 컴포넌트를 ThemeContext.Provider로 감싸줌
-> Provider의 모든 하위 컴포넌트가 얼마나 갚이 위치해 있는지 관계없이 컨텍스트의 데이터를 읽을 수 있음
//테마를 위한 컨텍스트를 생성하며, 기본값은 'light'
const ThemeContext = React.createContext('light');
//Provider을 사용하여 하위 컴포넌트들에게 현재 테마 데이터를 전달
//테마값으로 'dark'를 전달
function App(props) {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
//중간에 위치한 컴포넌트는 테마 데이터를 하위 컴포넌트로 전달할 필요가 없음
function Toolbar(props) {
return (
<div>
<ThemeButton />
</div>
);
}
function ThemeButton(props) {
//리액트는 가장 가까운 상위 테마 Provider을 찾아서 해당되는 값을 사용
//해당되는 Provider가 없을 경우 기본값을 사용
//여기에서는 상위 Provider가 있기 때문에 현재 테마의 값은 "dark"가 됨
return (
<ThemeContext.Consumer>
{value=><Button theme={value} />}
</ThemeContext.Consumer>
)
}
3. 컨텍스트를 사용하기 전에 고려할 점
- 무조건 컨텍스트를 사용하는 것이 좋은 것은 아님
-> 컴포넌트와 컨텍스트가 연동되면 재사용성이 떨어지기 때문
- 다른 레벨의 많은 컴포넌트가 데이터를 필요로 하는 경우가 아니라면 props를 통해 데이터를 전달하는 컴포넌트 합성 방법이 더 적합
ex)
- 가장 하위 레벨에 위치한 Avatar 컴포넌트가 user와 avatarSize를 필요로 하기 때문에, 이를 위해 여러 단계에 걸쳐서 props를 통해 user와 avatarSize를 전달해 주고 있는 상황
//page 컴포넌트는 PageLayout 컴포넌트를 렌더링
<Page user={user} avatarSize={avartarSize} />
//PageLayout 컴포넌트는 NavigationBar컴포넌트를 렌더링
<PageLayout user={user} avatarSize={avartarSize} />
//NavigationBar 컴포넌트는 Link 컴포넌트를 렌더링
<NavigationBar user={user} avatarSize={avartarSize} />
//Link 컴포넌트는 Avatar 컴포넌트를 렌더링
<Link href={user.permalink}>
<Avatar user={user} size={avatarSizes} />
</Link>
1) 엘리먼트 변수 사용
- user와 avartarSize가 props로 들어간 Avatar 컴포넌트를 userLink라는 변수에 저장한 뒤에 해당 변수를 하위 컴포넌트로 넘김
- 가장 상위 레벨에 있는 Page 컴포넌트만 Avatar 컴포넌트에서 필요로 하는 user와 avartarSize에 대해 알고 있으면 됨
function Page(props) {
const user = props.user;
const userLink = (
<Link href={user.permalink}>
<Avatar user={user} size={props.avatarSize} />
</Link>
);
//Page 컴포넌트는 PageLayout 컴포넌트를 렌더링
//이때 props로 userLink를 함께 전달
return <PageLayout userLink={userLink} />;
}
<PageLayOut userLink={...} />
//NavigationBar 컴포넌트는 props로 전달받은 userLink element를 리턴
<NavigationBar userLink={...} />
2) 하위 컴포넌트를 여러 개의 변수로 나눠서 전달
- 하위 컴포넌트의 의존성을 상위 컴포넌트와 분리할 필요가 있는 대부분의 경우에 적합
- 렌더링 전에 하위 컴포넌트가 상위 컴포넌트와 통신해야 하는 경우 render props를 사용하여 처리
function Page(props) {
const user = props.user;
const topBar = (
<NavigationBar>
<Link href={user.permalink}>
<Avatar user={user} size={props.avatarSize} />
</Link>
</NavigationBar>
);
const content = <Feed user={user} />;
return (
<PageLayout
topBar={topBar}
content={content}
/>
);
}
4. 컨텍스트 API
1) React.createContext
- 컨텍스트 생성
- 함수의 파라미터로 기본값을 넣어줌
- 컨텍스트 객체 리턴
- 리액트에서 렌더링이 일어날 떄, 컨텍스트 객체를 구독하는 하위 컴포넌트가 나오면 현재 컨텍스트의 값을 가장 가까이에 있는 상위 레벨의 Provider로부터 받아오게 됨
- 만약 상위 레벨에 매칭되는 Provider가 없다면, 기본값이 사용
- 기본값으로 Undefined를 넣으면 기본값이 사용되지 않음
const MyContent = React.createContext(기본값);
2) Context.Provider
- 모든 컨텍스트 객체는 Provider라는 리액트 컴포넌트를 갖고 있음
- Context.Provider 컴포넌트로 하위 컴포넌트들을 감싸주면 모든 하위 컴포넌트들이 해당 컨텍스트의 데이터에 접근할 수 있게 됨
<MyContext.Provider value={/*some value */}>
- Provider 컴포넌트에는 value라는 prop이 있으며, 이것은 Provider 컴포넌트 하위에 있는 컴포넌트들에게 전달
- 하위 컴포넌트들을 consumer 컴포넌트라고 부름
- consumer 컴포넌트는 컨텍스트의 값의 변화를 지켜보다가 만약 값이 변경되면 재렌더링됨
- 하나의 Provider 컴포넌트는 여러 개의 consumer 컴포넌트와 연결될 수 있으며 여러 개의 Provider 컴포넌트는 중첩되어 사용될 수 있음
3) Context.Consumer
- 컨텍스트의 데이터를 구독하는 컴포넌트
<MyContext.Consumer>
{value=> /*컨텍스트의 값에 따라서 컴포넌트들을 렌더링 */}
</MyContext.Consumer>
function as a child
- 컴포넌트의 자식(child)으로 함수를 사용하는 방법
- Context.Consumer로 감싸주면 자식으로 들어간 함수가 현재 컨텍스트의 value를 받아서 리액트 노드로 리턴하게 됨
- 함수로 전달되는 value는 Provider의 value prop과 동일
//children이라는 prop을 직접 선언
<Profile children={name => <p>이름: {name}</p> />
//Profile 컴포넌트로 감싸서 children으로 만듬
<Profile> {name=> <p>이름: {name}</p>} </Profile>
4) Context.displayName
- 컨텍스트 객체는 displayName이라는 문자열 속성을 가짐
- 크롬의 리액트 개발자 도구에서는 컨텍스트의 Provider나 Consumer을 표시할 때 displayName을 함께 표시
const MyContext = React.createContext(/*some value*/);
MyContext.displayName = 'MyDisplayName';
//개발자 도구에 "MyDisplayName.Provider"로 표시됨
<MyContext.Provider>
//개발자 도구에 "MyDisplayName.Consumer"로 표시됨
<MyContext.Consumer>
5. 여러 개의 컨텍스트 사용하기
- Context.Provider을 중첩해서 사용하는 방식으로 구현
ex)
- App 컴포넌트에서는 각 컨텍스트에 대해 두 개의 Provider를 사용하여 자식 컴포넌트인 Layout을 감싸줌
- 실제 컨텍스트의 데이터를 사용하는 Content 컴포넌트에서는 두 개의 Consumer 컴포넌트를 사용하여 데이터를 전달
//테마를 위한 컨텍스트
const ThemeContext = React.createContext('light');
//로그인을 한 사용자를 위한 컨텍스트
const UserContext = React.createContext( {
name: 'Guest',
});
class App extends React.Component {
render() {
const {singedInUser, theme } = this.props;
return (
<ThemeContext.Provider value={theme} >
<UserContext.Provider value={singedInUser}>
<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>
);
}
6. useContext
- useContext() 훅은 함수 컴포넌트에서 컨텍스트를 쉽게 사용할 수 있게 해줌
- useContext() 훅은 React.Context() 함수 호출로 생성된 컨텍스트 객체를 인자로 받아서 현재 컨텍스트의 값을 리턴
function MyComponent(props) {
const value = useContext(MyContent);
return (
...
)
}
7. 실습 - 컨텍스트를 사용하여 테마 변경 기능 만들기
ThemeContext 컴포넌트
- ThemeContext 생성
- 개발자 도구를 통해 컨텍스트 이름을 확인하기 위해서 ThemeContext의 displayName 값을 설정
import React from "react";
const ThemeContext = React.createContext();
ThemeContext.displayName = "ThemeContext";
export default ThemeContext;
MainContent 컴포넌트
- ThemeContext로부터 현재 설정된 테마 값을 받아와 실제 화면의 콘텐츠를 렌더링
- 테마 변경 버튼을 누를 경우 ThemeContext로 부터 받은 toggleTheme() 함수를 호출하여 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;
DarkOrLight 컴포넌트
- 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;
'react' 카테고리의 다른 글
[리액트를 다루는 기술] 3장 컴포넌트 (1) | 2023.11.01 |
---|---|
[소플] ch 15. 스타일링 (0) | 2023.11.01 |
[소플] ch 13. 합성 vs 상속 (0) | 2023.10.30 |
[소플] ch 12. State 끌어올리기 (0) | 2023.10.30 |
[소플] ch 11. 폼 (0) | 2023.10.29 |