보통 부모에서 자식 컴포넌트로 정보를 넘겨줄 때는 prop을 사용한다. 그러나 props는 때로 귀찮아진다. 부모 - 자식 컴포넌트 사이에 3개 - 4개 컴포넌트가 끼어 있는 경우 (고조할아버지 컴포넌트 ?), 같은 정보를 아주 많은 컴포넌트가 공유하고 있어야 할 경우 등.
Context 는 자식, 손주 컴포넌트들이 props로 안받아도 특정 정보를 부모, 조상에서 받아올 수 있게 해준다. (얼마나 먼 조상이든 상관없이!)
이 문서에서는..
- "prop drilling" 이 뭔지
- 반복되는 prop 넘겨주기를 context로 바꾸는 법
- context의 주요 사용 사례
- context의 흔한 대체재
를 배워보겠다.
아주 깊게 prop을 전달해주고 싶을 때
많은 컴포넌트가 같은 prop을 가지고 있어야 할 때
이런 경우 상태 끌어올리기를 사용해야 하는데, 정작 필요한 컴포넌트로부터 너무 멀리에서 선언돼야 할 수 있다. 이러면 Prop Drilling 상황이 생김.
이게 먼짓거리임. 필요한 컴포넌트보다 훨~~씬 위에서 선언하고 엄청 깊게 내려주는것 보다, 필요한 컴포넌트에 "텔레포트" 하는 방법이 있지 않을까?
그럴 때 Context를 써보자, 이말입니다.
Context는 부모 컴포넌트가 자기 하위의 전체 트리에 데이터를 제공하게 해준다.
아래 예시는 Heading
컴포넌트가 level
을 받아서 싸이즈로 사용하는 경우.
import Heading from './Heading.js';
import Section from './Section.js';
export default function Page() {
return (
<Section>
<Heading level={1}>Title</Heading>
<Heading level={2}>Heading</Heading>
<Heading level={3}>Sub-heading</Heading>
<Heading level={4}>Sub-sub-heading</Heading>
<Heading level={5}>Sub-sub-sub-heading</Heading>
<Heading level={6}>Sub-sub-sub-sub-heading</Heading>
</Section>
);
}
여기서 같은 Section
컴포넌트 아래에 있는 Heading
들은 모두 같은 싸이즈를 가지고 싶다고 해 보자.
import Heading from './Heading.js';
import Section from './Section.js';
export default function Page() {
return (
<Section>
<Heading level={1}>Title</Heading>
<Section>
<Heading level={2}>Heading</Heading>
<Heading level={2}>Heading</Heading>
<Heading level={2}>Heading</Heading>
<Section>
<Heading level={3}>Sub-heading</Heading>
<Heading level={3}>Sub-heading</Heading>
<Heading level={3}>Sub-heading</Heading>
<Section>
<Heading level={4}>Sub-sub-heading</Heading>
<Heading level={4}>Sub-sub-heading</Heading>
<Heading level={4}>Sub-sub-heading</Heading>
</Section>
</Section>
</Section>
</Section>
);
}
보다시피 level
이라는 prop으로 각 Heading
에 따로따로 내려준다.
<Section>
<Heading level={3}>About</Heading>
<Heading level={3}>Photos</Heading>
<Heading level={3}>Videos</Heading>
</Section>
level = {3}
을 세번 쓰지 말고, Section
컴포넌트에서 선언해주면 어떨까.
<Section level={3}>
<Heading>About</Heading>
<Heading>Photos</Heading>
<Heading>Videos</Heading>
</Section>
다 좋은데, <Heading>
컴포넌트 입장에서 가장 가까운 부모 <Section>
을 어떻게 알 수 있냐는 말이다.
여기서 Context 등장. 이 3단계를 따라하시라 :
LevelContext
라고 부르자.)Heading
이 LevelContext
를 사용)Section
이 LevelContext
를 제공)새로운 파일 만들어서 export 해주는 게 좋음. (관심사 분리)
// LevelContext.js
// createContext 훅 !
import { createContext } from 'react';
export const LevelContext = createContext(1);
createContext
훅이 받는 유일한 인자는 default 값이다. 객체같이 아무거나 넣어도 됨.
useContext
훅이랑 아까 만든 Context를 임포트하자.
// Heading.js
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';
지금 Heading
컨텍스트는 level
값을 props로부터 받고 있다. 이렇게 :
// Heading.js
export default function Heading({ level, children }) {
// ...
}
이렇게 하지 말고, level
prop을 없애고 아까 import 한 Context (LevelContext
)에서 받아오자. 즉 :
// Heading.js
export default function Heading({ children }) {
const level = useContext(LevelContext);
// ...
}
useContext
는 훅이다. useState
랑 useReducer
처럼, 훅은 리액트 컴포넌트의 최상단에서만 호출 가능함. useContext
는 리액트한테 Heading
컴포넌트가 LevelContext
를 읽어오고 싶다고 말해주는 것임.
이제 Heading
컴포넌트가 level
prop을 아지고 있지 않으니, 이렇게 prop을 넘겨줄 필요가 없다 :
// 이 코드가
// App.js
<Section>
<Heading level={4}>Sub-sub-heading</Heading>
<Heading level={4}>Sub-sub-heading</Heading>
<Heading level={4}>Sub-sub-heading</Heading>
</Section>
// 이렇게
// App.js
<Section level={4}>
<Heading>Sub-sub-heading</Heading>
<Heading>Sub-sub-heading</Heading>
<Heading>Sub-sub-heading</Heading>
</Section>
Section
에서 level
을 받으면 됨.
여기까지 읽었을 때 이상함을 느껴야 정상이다. 왜냐하면 Heading
에서 Context를 사용하긴 했지만, Section
에서 아직 제공하고 있지 않기 때문! 리액트가 Context를 어디서 가져와야 할지 아직 알지 못함.
이렇게 Context를 제공하지 않으면, 리액트는 맨 처음에 선언된 default 값 을 사용함. 이 예시에서는 createContext
의 인자로 1
을 줬다. 그래서 useContext(LevelContext)
는 1
을 리턴. 즉 모든 heading
들의 level 이 1
이 되니까 사실상 모든 컴포넌트가 <h1>
태그가 될 것이다.
이제 고쳐보자. 어떻게?
각 Section
이 자신의 Context를 자식들에게 제공하면 됨.
소외됐던 Section
컴포넌트를 다시 보면 :
export default function Section({ children }) {
return (
<section className="section">
{children}
</section>
);
}
children을 <section>
태그 안에 그대로 렌더링하는 녀석.
context provider
로 감싸서 LevelContext
를 제공해보자.
import { LevelContext } from './LevelContext.js';
export default function Section({ level, children }) {
return (
<section className="section">
<LevelContext.Provider value={level}>
{children}
</LevelContext.Provider>
</section>
);
}
리액트에게 :
" 이 <Section>
안에 있는 아무 컴포넌트가 LevelContext
를 요청하면, 이 level
을 줘. "
라고 말해주는 것이다! 그 컴포넌트는 그럼 가장 가까운 <LevelContext.Provider>
의 value
를 사용할 것이다.
이 경우 level
을 인자 prop으로 받았고, Provider
의 value
값에 level
을 넘겨줬으니, Heading
들은 가장 가까운 Section
의 Provider
, 그 중에서도 value
를 찾아올 것이다.
// App.js
import Heading from './Heading.js';
import Section from './Section.js';
export default function Page() {
return (
<Section level={1}>
<Heading>Title</Heading>
<Section level={2}>
<Heading>Heading</Heading>
<Heading>Heading</Heading>
<Heading>Heading</Heading>
<Section level={3}>
<Heading>Sub-heading</Heading>
<Heading>Sub-heading</Heading>
<Heading>Sub-heading</Heading>
<Section level={4}>
<Heading>Sub-sub-heading</Heading>
<Heading>Sub-sub-heading</Heading>
<Heading>Sub-sub-heading</Heading>
</Section>
</Section>
</Section>
</Section>
);
}
Heading
컴포넌트가 상위의 Section
컴포넌트에 요청해서 "자기가 알아서 찾아간다". 다시 정리 :
<Section>
에게 level
prop 넘겨주기<Section>
은 자식들을 <LevelContext.Provider value={level}>
로 감싼다.Heading
은 useContext(LevelContext)
를 통해 가장 가까운 상위 LevelContext
값을 찾아감.지금은 각 section의 level
을 어찌 됐건 수동으로 지정해줘야 한다.
// Page.js 또는 App.js
export default function Page() {
return (
<Section level={1}>
...
<Section level={2}>
...
<Section level={3}>
...
Context가 상위 컴포넌트의 정보를 읽게끔 하기 때문에, 각 Section
은 상위 Section
에서 level
을 읽어올 수 있고, 자동으로 level + 1
을 내려줄 수 있다. 이렇게 하면 된다 :
// Section.js
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';
export default function Section({ children }) {
const level = useContext(LevelContext); // Section 에서도 상위 level을 가져오자!
return (
<section className="section">
<LevelContext.Provider value={level + 1}> // 아래로 내려주기 위한 LevelContext를 내려주자!
{children}
</LevelContext.Provider>
</section>
);
}
이러면 level
prop을 <Section>
, <Heading>
둘 모두에게 내려주지 않아도 됨!
자동으로
상위 level
가져와서 자기가 쓰고, 자식한테는 level +
값을 내려줬기 때문.
import Heading from './Heading.js';
import Section from './Section.js';
export default function Page() {
return (
<Section>
<Heading>Title</Heading>
<Section>
<Heading>Heading</Heading>
<Heading>Heading</Heading>
<Heading>Heading</Heading>
<Section>
<Heading>Sub-heading</Heading>
<Heading>Sub-heading</Heading>
<Heading>Sub-heading</Heading>
<Section>
<Heading>Sub-sub-heading</Heading>
<Heading>Sub-sub-heading</Heading>
<Heading>Sub-sub-heading</Heading>
</Section>
</Section>
</Section>
</Section>
);
}
<div>
에 color:blue
속성을 붙이면, 하위 DOM은 깊이에 상관 없이 모두 그 속성을 상속하게 된다. 중간에 color:green
을 부여하지 않는 한!color
와 background-color
가 서로 다른 속성이라 서로 override 하지 못하는 것과 같이, Context도 다른 변수는 서로 섞이지 않음 (overuse 하지 말자. props가 더 간단한 것은 팩트임. 다음은 Context 도입 전에 고려할 대체재들이다.
props로 시작해라. 보통은 3개, 4개, 5개 넘는 하위 컴포넌트로 넘겨줄 일이 없으니깐.. 문서에 따르면 별것도 아닌데 Context 떡칠해놓으면 유지보수하는 개발자가 화낼 수 있다고 한다.. ㅋㅋ
컴포넌트를 추출하고, children
prop 을 잘 써라. Props로 내려줄 때 중간에 거쳐가는 컴포넌트가 많다면(=중간에 안쓰고 내려주기만 하는 컴포넌트들), 높은 확률로 중간에 컴포넌트를 추출 extract 하지 않았을 수 있다.
<App>
에서 <Layout>
안에 있는 <Posts>
컴포넌트로 posts
라는 prop을 내려주고 싶다. <Layout posts={posts} />
Layout
은 posts
prop을 쓰지도 않는데 받아서 다시 Posts
로 내려줘야 하는 것. 이러지 말고<Layout> <Posts posts={posts} /> </Layout>
요렇게 짠 다음, Layout
이 posts 말고 children
을 받게 해라! 데이터가 필요한 컴포넌트를 보다 직관적으로 볼 수 있음.Context는 자식 트리가 어떤 정보에 접근하게 제공해줌.
context를 내려주려면 :
export const MyContext = createContext(defaultValue)
로 생성, 추출useContext(MyContext)
훅에 인자로 넘겨준다.<MyContext.Provider value= {...}>
로 childern을 감싼다. 이제 children 안의 컴포넌트는 value 에 접근 가능.중간에 컴포넌트가 있어도 Context는 그대로 통과
Context를 쓰면 컴포넌트가 그때그때 주변에 맞게 적응하게 할 수 있음. 위에서 한 컴포넌트에서 Context 제공하고 사용하기 참조
Context 쓰기 전에 props로 넘겨주기 or children
으로 JSX 넘겨주기 해 볼 것.