🐕 Context API
contextAPI는 리액트 프로젝트에서 전역적으로 사용할 데이터가 있을때 유용한 기능이라고합니다.
그리고 이 기능은 리덕스 ,리액트 라우터, styled-components 등의 라이브러리에서도 사용한다고하네요
https://ko.reactjs.org/docs/context.html
공식 문서의 설명은 다음 링크에서 확인할 수 있습니다.
전 간략히 요약만 해드리겠습니다.
1. context를 이용하면 단계마다 일일이 props를 넘겨주지 않고도 컴포넌트 트리 전체에 데이터 제공가능
마치 확성기 같다.
위치에 상관없이 props로 넘겨주지 않아도 바로 데이터를 사용할 수 있다니
엥 이거 완전 상태관리 라이브러리아니냐?
즉 컴포넌트 전체에서 공유하며 많이 사용되는 데이터에 사용하기 적합할듯?
2. Context API를 남용하지 말것
공식문서에서도 props drilling을 막기위한 목적으로 Context API를 남용하지 말라고 설명해줍니다.
아니 이거만 봐서는 props drilling의 해결책으로 딱인데 왜 그런거임?
왜냐면 context를 사용하면 컴포넌트를 재사용하기가 어려워지기 때문입니다.
따라서 context의 용도는 다양한 레벨에 네스팅 된 컴포넌트에게 데이터를 전달하기 위한 용도이지만.
그 반대급부로 재사용성이 떨어지게 되니까 꼭 필요할때만 쓰고
props drilling의 해결책으로는 컴포넌트 합성을 하는 것이 더 간단한 해결책일 수 있으니
상황 봐가면서 써라. 라는 소리네요
컴포넌트 합성이 뭔지는 나중에... 얘기하는걸로...
3. Context API를 써야하는 시점
전역적으로 다양한 레벨에 있는 많은 컴포넌트에게 공유할 필요성이 있는 상태에 대해서만
Context API를 사용해주는 것이 좋다.
앞서 말했듯이 context를 사용하면 컴포넌트 재사용성이 떨어지기 때문
(왜 떨어지는지가 좀 궁금하다)
챗지피티한테 물어보면서 얻은 당연한 결론인데
당연히 다른 컴포넌트의 상태를 마구 사용하면 컴포넌트 간 의존성이 커질테고
다른 프로젝트에서 사용할 수 없어질 것이다.
(즉 재사용성이 떨어진다. 특정 컴포넌트에서 받아온 상태가 없으면 안되는 상황이 생길 수 있음)
👻Context API를 사용해보기 위한 억까 상황 설정
다크모드를 만들어보고 싶던 허수 개발자 냠냠맨
하위컴포넌트들에게 isDark 상태에 따라 다크모드를 켜고 끄고를 결정하고 싶은데..
컴포넌트가 너무 많던 나머지 isDark의 상태를 몰라도 상관없는 컴포넌트들도
자식 컴포넌트에게 정보를 넘겨주기 위해 isDark를 넘겨주고 넘겨주는 상황이 발생했다.
어떻게 해결하면 좋을까?
🥶 ContextAPI를 통해 해결하기
src / context / ThemeContext.js
import { createContext } from 'react';
export const ThemeContext = createContext(null);
contextAPI를 사용하기 위해선 먼저 이런식으로
createContext를 선행해주어야한다.
이것의 사용처는 곧 나옴
import { ThemeContext } from './context/ThemContext';
컨텍스트를 사용할 App.js로 돌아와 임포트를 해준 뒤
저 ThemeContext안에 대체 뭐가 들었을지 한번 열어보자.
어엄.... 뭔가 굉장히 많다 내가 null을 지정해줘서 null이 많이보이는 것 같으니
인자로 null 대신 "엄준식"을 전달해보도록하자
예상대로인 부분도 있지만 인자전달과 상관없이 null인 부분도 있는듯하다.
currentValue값은 내가 첫번째로 전달한 인자가 되는 것 같다.
Consumer | Consumer는 Provider에서 제공하는 Context 값을 구독하는 컴포넌트입니다. |
provider | Provider는 Context를 구독하는 컴포넌트들에게 context의 값을 제공합니다. 이를 통해 Provider 내부의 모든 하위 컴포넌트에서 해당 Context 값을 사용할 수 있습니다. |
defaultValue | defaultValue는 Provider가 값을 제공하지 않을 때 사용할 기본값입니다. Provider에서 값을 제공하지 않는 경우에 Consumer에서 defaultValue를 사용할 수 있습니다. |
displayName | Context 객체에는 displayName 프로퍼티가 있습니다. 이 값은 디버깅과 개발을 위해 사용됩니다. 이름을 지정하지 않으면, 개발 도구에서 Context를 식별하기 어려울 수 있습니다. |
GPT의 설명은 다음과 같다.
공식문서의 설명은 다음과 같다.
대충 value를 저런식으로 받아서 어떤 값을 넣어주면
그 값을 하위 컴포넌트에게 전달해준다는 의미인듯하다.
import React, { useState } from 'react';
import Page from './components/Page';
import { ThemeContext } from './context/ThemContext';
const App = () => {
const [isDark, setIsDark] = useState(false);
console.log(ThemeContext);
return (
<ThemeContext.Provider value={{ isDark, setIsDark }}>
<Page />;
</ThemeContext.Provider>
);
};
export default App;
사용은 기본적으로 이렇게하는듯하다.
creactContext의 결과를 담은것에서 Provider 프로퍼티의 밸류를 뽑아와서
컴포넌트로 사용하는데 저 ThemeContext.Provider 내부에 있는 컴포넌트들은
우리가 밸류로 전달해준 값들을 마치 전역적으로 사용하는 것처럼 사용할 수 있다는 것이다.
정말인지 한번 Page 컴포넌트로 넘어가보자.
import React, { useContext } from 'react';
import { ThemeContext } from '../context/ThemContext';
import Content from './Content';
import Footer from './Footer';
import Header from './Header';
const Page = () => {
const data = useContext(ThemeContext);
console.log(data);
return (
<div className="page">
<Header />
<Content />
<Footer />
</div>
);
};
export default Page;
페이지 컴포넌트는 하위 컴포넌트 세개를 갖고 있는 중간다리 역할을 해줄 컴포넌트다.
페이지 컴포넌트는 isDark의 상태가 궁금하지 않지만
우리가 Provider의 밸류로 넘겨준 값들이 잘 넘어왔는지는 궁금하니까 콘솔로그를 찍어보자.
하위 컴포넌트에서 ContextAPI로 넘겨준 값들을 사용하고 싶다면
위와 같이 useContext()를 이용해 받아올 수 있다.
data 변수에는 어떤 값이 담겨있을까?
정답은 우리가 넘겨준 밸류값이었습니다~!
정말 유익한 정보를 얻었지만 page 컴포넌트는 이 상태가 하나도 필요가 없다.
import React from 'react';
import Content from './Content';
import Footer from './Footer';
import Header from './Header';
const Page = () => {
return (
<div className="page">
<Header />
<Content />
<Footer />
</div>
);
};
export default Page;
지워주고 우리의 isDark 변수의 상태가 필요한 하위컴포넌트들에게 가서
ContextAPI를 사용해주자
Header.js
import React, { useContext } from 'react';
import { ThemeContext } from '../context/ThemContext';
const Header = () => {
const { isDark } = useContext(ThemeContext);
console.log(isDark);
return (
<header
className="header"
style={{
backgroundColor: isDark ? 'black' : 'lightgray',
color: isDark ? 'white' : 'black',
}}
>
<h1>안녕하쉐용</h1>
</header>
);
};
export default Header;
import React, { useContext } from 'react';
import { ThemeContext } from '../context/ThemContext';
const Footer = () => {
const { isDark, setIsDark } = useContext(ThemeContext);
const toggleTheme = () => {
setIsDark(!isDark);
};
return (
<footer
className="footer"
style={{
backgroundColor: isDark ? 'black' : 'lightgray',
}}
>
<button className="button" onClick={toggleTheme}>
Dark Mode{' '}
</button>
</footer>
);
};
export default Footer;
우린 이미 위에서 어떻게 값이 전달되는지도 봤으니까
(우리는 App.js에서 ThemeContext.Provider의 밸류로 객체를 넘겨줬었다!)
따라서 객체 디스트럭처링 할당을 통해 필요한 프로퍼티만 뽑아서 사용해주면 편할것이다.
그렇게 코드를 위와 같이 다 작성한 뒤
내 컴포넌트들이 잘 일하고 있는지 보러 가보자
내 의도대로 다크모드를 클릭하면 색이 바뀌도록 잘 구현이 되었다.
Props로 넘겨주는 것 없이도 이렇게 전역적으로 관리할 수 있으니 굉장히 편하다.
근데 어차피 이렇게 Provider에 값 넘겨줄건데 뭐하러 ThemeContext 안에 값을 넣음?
ThemeContext 안의 값은 밸류가 넘겨지지 않았을때 디폴트값으로 사용되는 값이라고 했읍니다.
이걸 어따 쓰는지는 의문이..?
지금 생각해봤을땐 디폴트값으로 넘겨줘놓고 밸류 전달을 안한다음 디폴트값만 활용해서 쓰면
좀 더 편할 것 같다? 정도네요
리액트를 다루는 기술을 보면서 다시 실습을 해보겠습니다.
🌞 새 Context 만들기
src/context/color.js
import { createContext } from 'react';
const ColorContext = createContext({ color: 'black' });
export default ColorContext;
아까처럼 createContext()를 통해 생성해줍니다.
아까 본것처럼 파라미터에 들어가는 값은 해당 Context의 기본상태값이다.(default다.)
그리고 이번엔 Consumer를 사용해봅니다.
😋Consumer 사용해보기
components/ColorBox.js
import React from 'react';
import ColorContext from '../context/color';
const ColorBox = () => {
return (
<ColorContext.Consumer>
{(value) => (
<div
style={{ width: '64px', height: '64px', background: value.color }}
></div>
)}
</ColorContext.Consumer>
);
};
export default ColorBox;
이번엔 Consumer 프로퍼티를 사용해봅니다.
근데 문법이 좀 안익숙한 것 같아요 저번에 NavList할 때도 그렇고
저 냅다 집어넣는 함수는 뭐며 저 안에 있는 밸류는 누가 전달해주는거지?
라고 생각을 했는데 이게 Render Props 패턴이라고 부르나봐요
위 예제를 보면 ColorContext.Consumer 컴포넌트 사이에
중괄호를 열어서 {}이 안에 냅다 함수를 넣어주는데
이러한 패턴을 Render Props 혹은 Function as a child 라고 부릅니다.
컴포넌트의 자식이 있어야할 자리에 일반 jsx 혹은 문자열이 아닌 함수를 전달하는걸 말해요!
import React, { useState } from 'react';
import ColorBox from './components/ColorBox';
import ColorContext from './context/Color';
const App = () => {
return (
<ColorContext.Provider value={{ color: 'red' }}>
<div>
<ColorBox />
</div>
</ColorContext.Provider>
);
};
export default App;
기본값은 Provider를 사용하지 않을 때에만 사용됩니다.
따라서 Provider를 사용했는데 value에 값을 명시해주지 않으면
기본값을 사용하지 않으면서 Provider안의 값도 없으니
컨텍스트 api를 사용하는 측에서 뭔가를 참조하려고하면 오류가 발생하겠네요
🤢Render Props를 이해하는 예제
const RenderPropsSample = ({childern}) => {
return <div>결과 : {Children(5)}</div>
}
export default RenderPropsSample
만약 위와 같은 컴포넌트가 있다면?
얘를 이렇게 사용할 수 있습니다.
<RenderPropsSample> {value => 2 * value}</RenderPropsSample>
아하... children props에 함수를 넣어주고
컴포넌트단에서 children을 객체디스트럭처링할당으로 받아와서
호출부에 값을 넣어주면 활용이 가능하다.
🤮동적으로 Context 사용해보기
import { createContext, useState } from 'react';
const ColorContext = createContext({
state: { color: 'black', subcolor: 'red' },
actions: {
setColor: () => {},
setSubcolor: () => {},
},
});
const ColorProvider = ({ children }) => {
const [color, setColor] = useState('black');
const [subcolor, setSubcolor] = useState('red');
const value = {
state: { color, subcolor },
actions: { setColor, setSubcolor },
};
return (
<ColorContext.Provider value={value}>{children}</ColorContext.Provider>
);
};
const { Consumer: ColorConsumer } = ColorContext;
export { ColorProvider, ColorConsumer };
export default ColorContext;
createContext()로 컨텍스트를 만들어준 다음
바로 같은 파일에서 Provider까지 짜버리는거임;;
그리고 아까 배운 renderProps를 이용해서 바로 컴포넌트를 리턴하겠다는거임;;
useState로 상태를 만들어놓고 그걸 객체안에 집어넣은다음
provider의 밸류로 그 객체를 집어넣고
children이 넘겨주는 함수에서 데이터를 처리해주면 되겠네요
저 객체 디스트럭처링 할당부분이 굉장히 혼란스러운데
const { Consumer: ColorConsumer } = ColorContext;
요 코드는
ColorConsumer = ColorContext.Consumer
를 해주겠다는 의미와 같다고 합니다..
이름을 붙여주는 느낌..?
import { ColorConsumer } from '../context/Color';
const colors = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet'];
const SelectColors = () => {
return (
<div>
<h2>색상을 선택하세용</h2>
<ColorConsumer>
{({ actions }) => (
<div style={{ display: 'flex' }}>
{colors.map((color) => (
<div
key={color}
style={{
background: color,
width: '24px',
height: '24px',
cursor: 'pointer',
}}
onClick={() => actions.setColor(color)}
onContextMenu={(e) => {
e.preventDefault();
actions.setSubcolor(color);
}}
></div>
))}
</div>
)}
</ColorConsumer>
</div>
);
};
export default SelectColors;
onContextMenu는 우클릭했을때의 이벤트를 설정해주네요!
처음 알았음;
import React, { useContext } from 'react';
import ColorContext, { ColorConsumer } from '../context/Color';
const ColorBox = () => {
const { state } = useContext(ColorContext);
return (
<>
<div
style={{
width: '64px',
height: '64px',
background: state.color,
}}
>
<div
style={{
width: '32px',
height: '32px',
background: state.subcolor,
}}
></div>
</div>
</>
);
};
export default ColorBox;
아까 써봤던 useContext를 사용하면 더 간결하게 사용할 수 있네요
rederProps 패턴을 사용하지 않으니까 좀 더 직관적인 느낌도 드네요
🐶마치며
Redux역시도 Context API를 기반으로 만들어졌다고 합니다...
그래서 잘 알아두면 아주 좋겠지만
아직도 저 Consumer가 잘 와닿지가 않네요 haah..
'react' 카테고리의 다른 글
React Router를 이용해보자 근데 실습을 곁들인 (0) | 2023.04.07 |
---|---|
react-virtualized 를 사용한 렌더링 최적화를 해보자 (0) | 2023.04.06 |
useEffect를 async와 함께 사용할 때 유의할 점 (0) | 2023.03.31 |
리액트의 SPA (SIngle Page Application) (0) | 2023.03.28 |
리액트의 성능을 최적화 해보자 프로젝트 (0) | 2023.03.27 |