자꾸 무언갈 부시는 호이초이 블로그를 보고 조심스레 제목을 베껴보았습니다 ㅎㅎ..
Context는 부모 컴포넌트가 트리 아래에 있는 모든 컴포넌트에 깊이에 상관없이 정보를 명시적으로 props를 통해 전달하지 않고도 사용할 수 있게 해줍니다.
공식문서에서 Context를 위와 같이 설명하고 있다.
쉽게 말해서 Context는 리액트 컴포넌트 트리 안에서 “전역적으로 값을 공유” 할 수 있게 해주는 기능이다.
즉, “부모 → 자식 → 손자 → 증손자” 이런 식으로 일일이 props로 넘기지 않고, “부모 → 손자”로 필요한 컴포넌트가 바로 값을 가져다 쓸 수 있게 하는 것이다.
Context 만들기
createContext() 라는 리액트에서 제공하는 함수를 이용하여 Context를 만든다.import { createContext } from 'react';
export const ThemeContext = createContext('light');useContext로 값을 찾을 때 부모에 값이 존재하지 않는 경우 초기값이 사용된다.Provider로 값을 감싼다
Context.Provider를 통해 하위 컴포넌트에 전달한다.import { ThemeContext } from './ThemeContext';
export default function App() {
const theme = 'dark';
return (
<ThemeContext.**Provider** value={theme}>
<Page />
</ThemeContext.**Provider**>
);
}값이 필요한 곳에서 useContext로 값을 꺼내 사용한다.
import { useContext } from 'react';
import { ThemeContext } from './ThemeContext';
export default function Page() {
const theme = useContext(ThemeContext);
return <div>현재 테마: {theme}</div>;
}
useConext()를 호출하는 컴포넌트는 그 위의 가장 가까운 컨텍스트의 value를 받게 된다.
먼가 위에 내용만 보면 Context를 사용하면 귀찮게 props를 뚫지 않아도 되니까 항상 Context를 사용하는 것이 좋아보인다. 하지만 실제로는 그렇지 않다. Context는 정말 필요할 때만 사용해야 한다.
그렇다면 무분별한 Context 사용의 단점을 알아보자.
이런 단점 때문에 무분별하게 Context를 사용하면 안된다.
여러 컴포넌트가 같은 값을 공유하는 경우
CardPreview 컴포넌트와 CardInfoForm 컴포넌트에서 같은 카드 정보 상태를 공유하는 경우function App() {
const { cardNumber, expirationPeriod, CVCNumber, password, cardType } =
useAllCardInfo();
return (
<StyledApp>
<StyledFrame>
<CardPreview
**// cardNumber={cardNumber.values}
// expirationPeriod={expirationPeriod.values}
// cardType={cardType.values.cardType}**
/>
<CardInfoForm
// **cardNumber={cardNumber}
// expirationPeriod={expirationPeriod}
// cardType={cardType}**
CVCNumber={CVCNumber}
password={password}
/>
</StyledFrame>
</StyledApp>
);
}props drilling(깊게 props를 계속 넘겨야 하는 문제)을 해결하고 싶은 경우
어떤 prop을 자식 컴포넌트를 통해 깊이 전해줘야 하거나, 많은 컴포넌트에서 같은 prop이 필요한 경우에 복잡하고 불편할 수 있다. 데이터가 필요한 여러 컴포넌트의 가장 가까운 공통 조상은 트리 상 높이 위치할 수 있고 “
Prop drilling”이라는 상황을 초래할 수 있다.
ex) CardInfoForm → Card~~Section → Card~~Inputs 로 props를 넘기는 경우
<CardInfoForm
**// cardNumber={cardNumber}**
expirationPeriod={expirationPeriod}
CVCNumber={CVCNumber}
password={password}
cardType={cardType}
/>
<CardNumberSection
**// cardNumber={cardNumber}**
cardNumberError={cardNumberError}
/>
<CardNumberInputs
**// cardNumber={cardNumber}**
cardNumberError={cardNumberError}
/>
하지만 공식문서에서 props drilling이 발생한다고 Context를 무조건적으로 사용하는 것을 우려하고 있다.
아래 props drilling + 아래 2가지를 만족하지 않을 때 context를 사용하는 것을 권장?하고 있다.
- Props 전달하기로 시작하기. 사소한 컴포넌트들이 아니라면 여러 개의 props가 여러 컴포넌트를 거쳐 가는 것은 그리 이상한 일이 아닙니다. 힘든 일처럼 느껴질 수 있지만 어떤 컴포넌트가 어떤 데이터를 사용하는지 매우 명확히 해줍니다. 데이터의 흐름이 props를 통해 분명해져 코드를 유지보수 하기에도 좋습니다.
- 컴포넌트를 추출하고 JSX를
children으로 전달하기. 데이터를 사용하지 않는 많은 중간 컴포넌트 층을 통해 어떤 데이터를 전달하는 (더 아래로 보내기만 하는) 경우에는 컴포넌트를 추출하는 것을 잊은 경우가 많습니다. 예를 들어posts처럼 직접 사용하지 않는 props를<Layout posts={posts} />와 같이 전달할 수 있습니다. 대신Layout은children을 prop으로 받고<Layout><Posts posts={posts} /><Layout>을 렌더링하세요. 이렇게 하면 데이터를 지정하는 컴포넌트와 데이터가 필요한 컴포넌트 사이의 층수가 줄어듭니다.
쉽게 요약하면 아래와 같다.
결국 “props로 자연스럽게 해결할 수 없고, 많은 컴포넌트가 같은 데이터를 써야 한다면 Context를 고민해보자.” 라고 정리할 수 있을 거 같다.
리액트 공식문서에서 예시로 든 Context 사용 방법은 아래와 같다.
데이터를 지정하는 컴포넌트와 데이터가 필요한 컴포넌트 사이의 층수 줄이기
<CardNumberSection
cardNumber={cardNumber}
cardNumberError={cardNumberError}
/>
<CardNumberSection>
<CardNumberInputs
cardNumber={cardNumber}
cardNumberError={cardNumberError}
/>
</CardNumberSection>
Context 사용하기
import { createContext } from 'react';
export const CardInfoContext = createContext(null);
function App() {
const { cardNumber, expirationPeriod, CVCNumber, password, cardType } =
useAllCardInfo();
return (
<StyledApp>
<StyledFrame>
<CardPreview
cardNumber={cardNumber.values}
expirationPeriod={expirationPeriod.values}
cardType={cardType.values.cardType}
/>
<CardInfoForm
cardNumber={cardNumber}
expirationPeriod={expirationPeriod}
CVCNumber={CVCNumber}
password={password}
cardType={cardType}
/>
</StyledFrame>
</StyledApp>
);
}
function App() {
const cardInfo = useAllCardInfo();
return (
<StyledApp>
<StyledFrame>
<CardInfoContext.**Provider** value={cardInfo}>
<CardPreview />
<CardInfoForm />
**</CardInfoContext.Provider>**
</StyledFrame>
</StyledApp>
);
}
value로 cardInfo ****전체를 넘기고 있어서, 만약 카드 번호만 변경되어도 cardInfo(유효 기간, 비밀 번호 등등..)를 구독하는 모든 컴포넌트가 리렌더링이 발생한다. 이는 매우 비효율적이다.
그러면 Context를 잘게 쪼개면 되는거 아니야? 라는 의문이 생긴다. 하지만 이렇게 되면 아래 코드와 같이 Provider 지옥이 발생한다.
<UserContext.Provider value={user}>
<ThemeContext.Provider value={theme}>
<NotificationContext.Provider value={notifications}>
<SettingsContext.Provider value={settings}>
<LanguageContext.Provider value={language}>
{/* 실제 화면 */}
</LanguageContext.Provider>
</SettingsContext.Provider>
</NotificationContext.Provider>
</ThemeContext.Provider>
</UserContext.Provider>
상태(State)란 어플리케이션의 작동에 관여하는 모든 데이터를 말한다. 중요한 것은 데이터가 필요 시 저장되고(stored), 읽히고(read), 업데이트되고(updated), 사용되어야(used) 한다는 점이다.
그런 의미에서 상태 관리란
를 의미한다고 할 수 있다. 일반적으로 현재 값이 변경되면 알리는 역할도 포함한다. 리액트의 useState와 useReducer 훅은 상태 관리의 좋은 예시가 된다.
이런 맥락에서 봤을 때 Context는 상태 관리 툴이 아니다. Context는 그 자체로는 아무것도 '저장'하지 않는다. 상위 컴포넌트가 <MyContext.Provider>를 렌더하는 상위 컴포넌트는 Context에 어떤 값을 넣어줄지 결정하는 것에만 관여한다. 그리고 이 값은 보통 리액트 컴포넌트의 상태에 기반한다. 실질적인 '상태 관리'는 useState/useReducer 훅으로 이루어지는 것이다.
결국, 컨텍스트는 이미 어딘가에 존재하는 상태를 다른 컴포넌트와 공유하는 방법일 뿐이다.
그래서 Context 만을 사용해서는 실제로 상태를 관리할 수는 없다. 공식문서에서는 Context를 사용해 상태 관리하기 위해 Context와 reducer를 함께 쓰기를 권장한다.
https://velog.io/@woogur29/React-Context의-내부-동작-원리