useReducer와 contextAPI의 사용법을 알아보자.
이전 04.상태와 리렌더링 맛보기를 통해 useReducer를 만들었었다. 그때도 원하는 대로 구현되었지만 내 코드를 믿지 못하고 진짜 useReducer는 무언가 다를 것이라고 기대했던 것 같다. 당장 setState 함수를 분리하는 것 외에는 큰 장점을 느끼지 못했다.
useReducer는 useState를 대체한다. setState 함수를 외부로 분리할 수 있지만, 호출은 컴포넌트에서 이루어진다. 초기값 또한 컴포넌트가 갖게 된다. 그러다 보니 1개의 컴포넌트에 여러 개의 setState가 있을 때가 아니고서는 엄청나게 도움이 되는 것 같지 않았다. 더 필요한 것은 props drilling을 줄이는 것이었고, useReducer와 contextAPI 조합으로 해결할 수 있었다.
useState와 동일한 기능을 구현하고 있으면서도 대체 기술이라는 생각은 해보지 않았다. 작은 부분이라도 다른 것이 있으니까 구분되어 사용될 것이라며 아예 다르게 분류해 버렸다. 공부하면 할수록 관련 기술들은 완전히 다른 것보다 조금씩 보완되는 것들이 더 많다는 것을 느낀다. 가끔은 이 미세한 차이가 괴롭다. 어떤 것을 선택하는 게 확실히 더 좋다는 정답이 없는 것이 결정을 어렵게 한다.
클라우드 서비스를 이용해 데이터를 올려두고 언제 어디서든 접근할 수 있다. props drilling을 해결하기 위해 상태 관리를 여러방면으로 다루어보면서 전역 상태를 관리하기 위한 기술이 클라우드 서버 같다고 느꼈다. 클라우드는 편리하지만 관리할 데이터 양이 적다면 필수 아닌 선택이 된다.
처음 상태 관리를 접할 때는 "라이브러리는 필수다" 로 받아 들여서 다짜고짜 redux를 갖다 붙였던 기억이 난다. 프로젝트 규모, 기능, 컴포넌트가 필요로 하는 데이터에 따라 상태 클라우드☁️의 필요성을 판단해 볼 필요가 있다.
useReducer는 상태 클라우드의 개념은 아니다. 단지 reducer들을 외부에서 모아 관리할 수 있을 뿐이다. 그래서 useState 사용과 비교했을 때, setState 함수가 외부에서 분리되어 관리되는 것 외에 컴포넌트별 상태들은 크게 다르지 않다. 처음 useReducer를 적용할 때는 분리를 하다보니 당연히 전역적으로 dispatch를 호출할 수 있을 것이라 생각했다. useState와 동일하게 prop으로 전달해주어야 한다.
useReducer를 호출하면서 action들의 모음인 reducer 함수와 initialState를 전달한다. reducer 함수는 initialState를 참조하여 action을 통해 새로운 state를 반환한다.
정확히는 <Context.provider/>
이지만 간단히 <Provider/>
로 표현했다. 상단 context 부분이 useReducer와 구조는 동일하지만 호출을 어디에서 하고 초기값을 누가 들고 있냐가 달라진다. context는 컴포넌트가 아닌 외부에서 initialState를 관리하고, 이 값을 이용해 action을 실행한다. 클라우드로 감싸진 컴포넌트들은 모두 데이터에 접근할 수 있다. 필요한 컴포넌트에서 데이터를 가져다 쓸 수 있기때문에 전달만 했던 <Section/>
컴포넌트는 쉴 수 있다.
const [state, dispatch] = useReducer(리듀서함수, 초기값);
데이터가 필요한 컴포넌트에서 호출하며, reducer 함수와 초기값을 인자로 전달한다. 액션들을 모아둔 리듀서 함수를 useReducer 내부에서 1개의 dispatch 함수로 반환하는 것이다.
reducer 함수 생성
setState들을 모아둔 reducer 함수 선언부를 외부 파일에서 관리한다.
reducers라는 폴더에 관련된 이름의 js 파일로 작성한다.
export const 리듀서함수명 = (state, action) => {
switch (action.type) {
case dispatch로호출할type명: {
return 값
}
case
...
};
매개변수 state는 useReducer를 호출할 때 전달 받은 초기값이고, action은 반환값 2번째 배열의 dispatch 함수로 전달된 인자이다.
state와 dispatch 사용
dispatch({ type: "type명", state 변경을 위한 값 });
dispatch를 호출하면 useReducer로 전달된 reducer 함수에 위 코드의 { type, state 변경을 위한 값 }
이 객체로 전달되며, 설정된 이름인 action에 담긴다. dispatch로 state 값이 변경되면 useReducer의 호출로 반환된 state가 갱신된다.
//2
const NameContext = createContext();
//3
export const NameProvider = ({ children }) => {
return (
<NameContext.Provider value={value}>
{children}
</NameContext.Provider>
);
};
function App() {
return (
<SectionProvier>
<Template>
<Header />
<CheckList />
</Template>
</SectionProvier>
);
}
꼭 App 컴포넌트가 아니더라도 데이터를 공유하고 싶은 컴포넌트가 있다면 감싸줄 수 있다.
const value = useContext(createContext로만든Context);
useContext는 클라우드 공급자(Provider) 정보를 받아서 해당 클라우드가 가지고 있는 value를 반환한다.
함께 사용하면 무엇이 좋을까? context 클라우드를 만들면 state와 action을 설정해야 한다. 이때, useReducer를 활용하면 action들을 외부 파일 또는 같은 파일이지만 분리해서 reducer 함수로 관리할 수 있다.
export const NameProvider = ({ children }) => {
//useRedcuer
const [state, dispatch] = useReducer(reducer함수, initialState);
const value = {
state,
dispatch,
}
return (
<NameContext.Provider value={value}>
{children}
</NameContext.Provider>
);
};
//사용
const value = useContext(createContext로만든Context);
value.state, value.dispatch()
value는 예제처럼 1개의 객체로 전달할 수도 있고, prop으로 펼쳐서 전달할 수도 있다.
클라우드에 전달할 value에 state와 dispatch를 바로 전달하지 않고 내부에서 dispatch를 적용한 action 함수를 만들고, 그 함수를 전달할 수도 있다. 그럼 사용하는 곳에서는 type을 명시하지 않고, 인자만 전달할 수 있게 된다.
export const NameProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer함수, initialState);
//action 함수
const addSection = (inputType, name, title)=>{
dispatch({ type: "ADD_SECTION", inputType, name, title })
}
const value = {
state,
//actions 객체로 묶어주기
actions: {
addSection,
...
}
}
return (
<NameContext.Provider value={value}>
{children}
</NameContext.Provider>
);
};
//사용
const value = useContext(createContext로만든Context);
value.state, value.actions.addSection()
reducer 함수로 관리될 action들이 많지 않고 간단하다면 useReducer 없이 함수로 작성해 actions로 전달하는 방법도 있다.
컴포넌트에서 context 클라우드의 데이터에 접근하기 위해서는 useContext를 호출해야 하고, 그 인자로 createContext로 만든 context를 전달해야 한다. 이는 context가 많거나 복잡하면 불편할 수 있다.
export const useSectionState = () => {
return useContext(sectionStateContext);
};
context 클라우드를 만든 파일 안에 커스텀훅을 만든다. 반환값이 useContext가 되어 컴포넌트에서는 별도의 인자를 받지 않는 훅을 호출하기만 하면 된다.
Provider를 이용해 클라우드를 적용하면 value prop으로 데이터를 가져다 쓸 수 있다. 이때 문제는 value는 객체 타입으로 state와 action(setState)이 함께 있기 때문에 어떤 컴포넌트에서 state만 가져다 사용해도, action 중 1개만 사용해도 구독중인 모든 컴포넌트가 리렌더링 된다. 이 문제는 state와 action 각각의 Provider를 만들어 해결할 수 있다.
export const SectionProvier = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<stateContext.Provider value={state}>
<dispatchContext.Provider value={dispatch}>
{children}
</dispatchContext.Provider>
</stateContext.Provider>
);
};
context가 많아지면 복잡해질 것 같은 느낌이다. React.memo, useMemo 등 여러방면으로 보완하는 방법들을 찾아 볼 수 있지만 매번 체크하는 것도 쉽지 않을 것 같다. 데이터를 분리해서 사용하는 경우가 많은 프로젝트 구조를 갖는다면 상태 관리는 다른 방법을 활용하는 것이 좋겠다.