React로 앱을 제작하다 보면, 전역변수를 사용하지 않아서 불편할 때가 많다.
내가 지금 하고 있는 React Todo App 만들기 또한 여러 가지 컴포넌트를 쓰고 싶은데 App.js에다가 모든 정보를 다 몰아넣어 놓고 애플리케이션을 만들려다 보니까 랜더링이나 컴포넌트의 깊이 등등 신경써야 하는 부분이 많아서 불편한 점이 한두가지가 아니다. 이런 점을 개선하기 위하여 Context API를 사용할 수 있는데 ,이번 기회에 해당 API를 사용하여 보려고 한다.
먼저 context API를 사용하기 위해서 src
폴더에 구분할 수 있는 contexts
폴더를 생성하였다.
import { createContext } from 'react';
const ColorContext = createContext({ color : 'black'});
export default ColorContext;
이렇게 context 관련한 폴더를 생성할 수 있었고, 새로운 context를 생성하는 것이니까 createContext
함수를 사용하였다.
그럼 이러한 context를 사용하기 위해서는 어떻게 해야 할까?
component에 colorBox라는 이름을 가진 컴포넌트를 만들어보자.
import React from 'react';
import ColorContext from '../contexts/color';
const ColorBox = () => {
return (
<ColorContext.Consumer>
{value => (
<div
style = {{
width : '64px',
height : '64px',
background : value.color
}}
/>
)}
</ColorContext.Consumer>
);
};
export default ColorBox;
이렇게 코드를 작성할 수 있는데, ColorContext
안에 들어 있는 색상을 보여주는 것입니다.
이 때 색상을 props
로 받아 오는 것이 아니라, ColorContext
안에 들어 있는 Consumer
라는 컴포넌트를 통하여 색상을 조회할 수 있다.
Consumer
라는 것은 잘 들어 있는 Context의 내용을 사용하기 때문에 이렇게 이름지은 게 아닐까 싶다.
Consumer
사이에 중괄호를 열어서 그 안에 함수를 넣어 주었는데, 이러한 패턴을 Function as a child
혹은 Render Props
라고 한다.
컴포넌트의 children이 있어야 할 자리에, 일반 JSX 혹은 문자열이 아닌 함수를 전달하였기 때문이다.
Provider를 사용하게 된다면 Context의 value를 변경할 수 있다.
import ColorBox from "./components/ColorBox";
import ColorContext from './contexts/color';
const App = () => {
return (
<ColorContext.Provider value = {{ color : 'red'}}>
<div>
<ColorBox/>
</div>
</ColorContext.Provider>
)
}
export default App;
이렇게 코드를 변경하고 나면 이렇게 랜더링이 된다.
기존에 createContext
함수를 사용할 때에는 파라미터로 Context의 기본값을 넣어 주었는데. 기본값은 Provider
를 사용하지 않았을 때에만 사용됩니다.
만약 Provider를 사용하였는데, value를 명시하지 않았더라면 기본값을 사용하지 않기 때문에 오류가 발생하므로, Provider 사용시에는 꼭 value 명시해주기~
import ColorBox from "./components/ColorBox";
import ColorContext from './contexts/color';
const App = () => {
return (
<ColorContext.Provider>
<div>
<ColorBox/>
</div>
</ColorContext.Provider>
)
}
export default App;
이렇게 쓰면 오류가 발생해요. 꼭 value 값 함께 써주기!
context의 value에는 무조건 고정된 상태 값만이 있는 것이 아니고, 함수를 전달해 줄 수도 있다.
기존에 작성했던 Colorcontext 코드를 수정해보자.
const ColorContext = createContext({
state : { color : 'black', subcolor : 'red'},
actions : {
setColor : () => {},
setSubcolor : () => {}
}
});
먼저 기존에 state만 존재하였던 ColorContext
를 위와 같이 고쳐 주었다. actions
를 추가하여서 어떤 동작을 하는지 보여주는 것이다. 그리고 ColorProvider
라는 컴포넌트를 새로 작성하였다. ColorProvider
라는 컴포넌트는 ColorContext.Provider
를 랜더링하고 있다.
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>
);
};
이 Provider의 value에는 상태는 state로, 업데이트 관련한 것은 actions로 묶어서 전달핟고 있다.
꼭 항상 묶어서 값을 전달해야 하는 것은 아니지만~ 이렇게 state와 actions를 따로 분리해주면 나중에 재사용할 때 편리하다는 점!!
추가로, createContext
를 사용할 때 기본적으로 사용할 객체도 수정했다. createContext
의 기본값은 실제로 Provider
의 value
에 넣는 객체의 형태와 일치시키는 것이 좋다. 이렇게 하면 Context 코드 내부의 값이 어떻게 구성되어 있는지 확인하기도 편하고, 실수로 Provider를 사용하지 않을 때 리액트에서 에러가 발생하지 않기 때문이다 .이렇게 개선한 Context를 한번 반영해보자.
import ColorBox from "./components/ColorBox";
import { ColorProvider } from "./contexts/color";
const App = () => {
return (
<ColorProvider>
<div>
<ColorBox/>
</div>
</ColorProvider>
)
}
export default App;
기존에 <ColorContext.Provider>
이렇게 쓰던거를 위처럼 간단하게 작성할 수 있다.
그리고 color.js
도 아래와 같이 변경 가능하다.
import React from 'react';
import { ColorConsumer } from '../contexts/color';
const ColorBox = () => {
return (
<ColorConsumer>
{value => (
<div
style = {{
width : '64px',
height : '64px',
background : value.color
}}
/>
)}
</ColorConsumer>
);
};
export default ColorBox;
Provider.Consumer
라고 쓰던 코드를 위와 같이 깔끔하게 바꿀 수 있다. 그리고 안에 있는 함수 꼴도 바꾸어보자.
import React from 'react';
import { ColorConsumer } from '../contexts/color';
const ColorBox = () => {
return (
<ColorConsumer>
{value => (
<>
<div
style = {{
width : '64px',
height : '64px',
background : value.state.color
}}
/>
<div
style = {{
width : '32px',
height : '32px',
background : value.state.subcolor
}}
/>
</>
)}
</ColorConsumer>
);
};
export default ColorBox;
이렇게 코드를 작성할 수 있다. 비구조화 할당을 이용하여 value
를 조회하는 것을 생략할 수 있다.
import React from 'react';
import { ColorConsumer } from '../contexts/color';
const ColorBox = () => {
return (
<ColorConsumer>
{({state})=> (
<>
<div
style = {{
width : '64px',
height : '64px',
background : state.color
}}
/>
<div
style = {{
width : '32px',
height : '32px',
background : state.subcolor
}}
/>
</>
)}
</ColorConsumer>
);
};
export default ColorBox;
Context의 actions에 넣어 준 함수를 호출하는 컴포넌트를 만들어보겠습니다.
const colors = ['red', 'oragne', 'yellow', 'green', 'blue', 'indigo', 'violet'];
const SelectColors = () => {
return (
<div>
<h2>색상을 선택하세요.</h2>
<div style = {{ display : 'flex'}}>
{colors.map(color => (
<div
key = {color}
style = {{
background : color,
width : '24px',
height : '24px',
cursor : 'pointer'
}}
/>
))}
</div>
<hr />
</div>
);
};
export default SelectColors;
이것을 App.js에 랜더링 시켜주면 이렇게 귀엽게 화면에서 색을 선택할 수 있게끔 색이 나열되어 있습니다.
색을 선택할 수 있는 목록을 만들어 보도록 하자.
import { ColorConsumer } from "../contexts/color";
const colors = ['red', 'oragne', '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>
)}
<hr />
</ColorConsumer>
</div>
);
};
export default SelectColors;
이렇게 코드를 작성하게 되면 마우스 왼쪽 버튼을 눌렀을 때 설정되는 색은 color
에 저장되고, 마우스 오른쪽 버튼을 눌렀을 때 기본적으로 복사 박스 같은 것이 드러나니까, preventDefault
를 통하여 기존에 발생하는 이벤트를 예방할 수 있도록 하고, subColor
를 설정하도록 한다.
Context에 있는 값을 사용할 때, Consumer를 사용하지 않고 가져올 수 있는 방법에 대해서 고민해보자.
아주 편하게 context를 사용할 수 있는 방법 중의 하나이다.
Consumer
사용하던 부분을 다 지워주고!
import React, { useContext } from 'react';
import ColorContext from './../contexts/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
}}
/>
</>
);
};
export default ColorBox;
이전보다 훨씬 간결해졌다. chilren
에 직접적으로 전달해주는 Render Props
패턴이 나는 개인적으로 불편하기 때문에 useContext
를 자주 사용하곤 한다. 그런데 이것은 함수 컴포넌트에서밖에 사용할 수 없다.
기존에는 컴포넌트 간의 상태를 교류해야 할 때에는 무조건 부모 -> 자식 흐름으로 props를 통하여 전달하였다. 그런데 우리는 context API를 통하여 더욱 쉽게 상태를 교류할 수 있게 되었다.
전역적으로 사용되는 상태가 있고, 컴포넌트의 개수가 많은 상태라면 Context API를 사용하는 것이 맞다.
원래 상태관리의 정석은 Redux를 사용하는 것인데, 단순한 전역관리만 한다면 Context API로 리덕스를 대체할 수 있다. 하지만 리덕스는 성능이 더욱 좋고, 미들웨어 기능, 개발자 도구까지 있어서 유지보수가 좋아 리덕스가 좋을 때도 있다는 점을 기억하며 상황에 맞게 사용해야겠다