Reducer 와 Context 를 같이 써보자 - new 리액트 공식문서

hongregii·2023년 3월 20일
0
  • reducer : state 업데이트 로직을 통합
  • context : 깊은 하위 컴포넌트에 정보 넘겨줌

둘을 같이 쓰면 무적의 state 관리자가 되겠지요?

이 문서에서는

  • reducer와 context 같이 쓰기
  • props로 state 넘기고 dispatch 하지 않는 법
  • 관심사의 분리 : context랑 state logic을 다른 파일에서 관리하는 법
    을 배워보겠다.

Reducer를 Context와 함께

이전 문서 - Reducer에 State 로직 추출하기 에서는 state를 reducer로 관리했다. reducer 함수에는 state 업데이트 로직이 모두 들어있고, App.js 맨 밑에서 선언했다.

코드 임포트가 어려워 공식문서를 링크로 걸어놓겠다.
공식문서 - Scaling Up with Reducer and Context

Reducer 가 있어 이벤트 핸들러 함수가 간결해졌다. 그러나, 어플리케이션이 커지면 또 다른 문제점이 생김.

현재는 tasks state와 dispatch 함수가 최상단 TaskApp 컴포넌트에서만 사용 가능하다는 것!

하위의 다른 컴포넌트들이 이 state를 읽어오고 싶거나 변경하고 싶다면? prop으로 dispatch 함수랑 tasks state를 쭈우우욱 내려줘야 한다는 말이다. 귀찮겠지?

해결책은 바로 context. TaskApp 컴포넌트 하위의 모든 녀석들에서 반복적인 prop drilling (props 불필요하게 쭈우우우욱 내려주기) 없이 이 state를 마음껏 불러오고 변경할 수 있게 된다.

reducer를 context와 함께 쓰려면 이렇게 하자.

  1. Context를 만드시오
  2. state와 dispatch를 Context에 넣으시오
  3. 하위 트리에서 Context를 쓰시오

1단계 : Context를 만드시오

useReducer 훅을 쓰자. 현재 상태 tasksdispatch 함수를 리턴해준다.

const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

이것들을 트리 하위로 내려주려면, Context를 두개 만들어야 함.

  • TasksContext : 현재 tasks state 리스트를 하위에 제공
  • TasksDispatchContext : dispatch 함수를 하위에 제공

Context는 별도 파일로 만들어라 - 그래야 import 하기 쉬워짐.

// TasksContext.js

import { createContext } from 'react';

export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);

두 context 모두 null을 default 값으로 넣었다. 실제 값은 TaskApp 컴포넌트에서 줄거임.

2단계 : state와 dispatch를 Context에 넣으시오

TaskApp 컴포넌트에서 두 Context를 임포트해올 수 있다. useReducer()에서 리턴되는 tasks, dispatch를 하위 트리에 제공하자. provide

// TaskApp.js
import { TasksContext, TasksDispatchContext } from './TasksContext.js';

export default const TaskApp = () {
  const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
// ... 사실 이까지는 기존 reducer 로직. initalTasks는 맨 밑에 선언돼있었다.
  
  return (
    <TasksContext.Provider value={tasks}>
      <TasksDispatchContext.Provider value = {dispatch}>
        // ... 어쩌구저쩌구 children들
      </TasksDispatchContext.Provider>
    </TasksContext.Provider>
  );
}

이제 ... 어쩌구저쩌구 childern들 자리에 들어오는 하위 트리의 컴포넌트들이 모두 tasksdispatch 함수를 사용할 수 있게 됨.

3단계 : 하위 트리 어디서나 Context를 쓰시오

Context 적용 전 기존 코드 :

// App.js
return (
   <AddTask
        onAddTask={handleAddTask}
      /> // props로 dispatch 함수를 넘겨주고 있다.
      <TaskList
        tasks={tasks} // props로 tasks  state를 넘겨주고 있다.
        onChangeTask={handleChangeTask} // props로 dispatch 함수를 넘겨주고 있다2.
        onDeleteTask={handleDeleteTask} // props로 dispatch 함수를 넘겨주고 있다3.
      />
);

context 적용 후 코드 :

// App.js
<TasksContext.Provider value={tasks}>
  <TasksDispatchContext.Provider value={dispatch}>
    <h1>Day off in Kyoto</h1>
    <AddTask /> // props 가 빠졌다. Provider에서 불러올 수 있기 때문
    <TaskList /> // props 가 빠졌다. Provider에서 불러올 수 있기 때문2
  </TasksDispatchContext.Provider>
</TasksContext.Provider>
// TaskList.js
export default function TaskList() {
  const tasks = useContext(TasksContext); // useContext 훅으로 tasks state를 불러오고 있다.
  // ...
// AddTask.js
export default function AddTask() {
  const [text, setText] = useState('');
  const dispatch = useContext(TasksDispatchContext); // useContext 훅으로 dispatch를 불러오고 있다.
  // ...
  return (
    // ...
    <button onClick={() => {
      setText('');
      dispatch({
        type: 'added',
        id: nextId++,
        text: text,
      });
    }}>Add</button>
    // ...

State는 여전히 최상위 TaskApp 컴포넌트에 "살고 있다" - useReducer의 관리를 받을 뿐.

그러나 Context를 사용해서 하위 어디서나 접근할 수 있는 것.

전선 정리 : reducer, context 관련 로직은 모두 한 파일로 옮겨놓자!

굳이 할 필요는 없는 작업이긴 하다만, reducer 와 context를 한 파일에 때려박으면 코드 보기가 조금 더 편해진다. 현재 TasksContext.js에는 Context 선언문 두개밖에 없지만... 점점 많아질 것이다.

  • reducer를 여기에 옮겨놓고,
  • 새로운 TasksProvider 컴포넌트를 여기서 선언하자. 이 컴포넌트는 모든 조각을 잇는다 무스비
    1. reducer로 state 관리
    2. 위에서 선언된 두 Context를 하위에 제공 provide
    3. prop에 childeren 넣어서 JSX를 받을 수 있음.
// TasksContext.js -> TasksProvider.js
export function TasksProvider({ children }) {
  const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

  return (
    <TasksContext.Provider value={tasks}>
      <TasksDispatchContext.Provider value={dispatch}>
        {children}
      </TasksDispatchContext.Provider>
    </TasksContext.Provider>
  );
}

이러면 TaskApp 컴포넌트의 복잡한 전선들이 사라진다.

// TaskApp.js
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
import { TasksProvider } from './TasksContext.js';

export default function TaskApp() {
  return (
    <TasksProvider>
      <h1>Day off in Kyoto</h1>
      <AddTask />
      <TaskList />
    </TasksProvider>
  );
}

Custom Hooks로 관리하자

우리가 만든 Context를 use 사용 하는 함수를 export할 수 있다.

export funciton useTasks() {
  return useContext(TasksContext);
}

export function useTasksDispatch() {
  return useContext(TasksDispatchContext);
}

컴포넌트가 context를 읽어올 필요가 있을 때, 이렇게 넘겨주면 된다:

const tasks = useTasks();
const dispatch = useTasksDispatch();

파일 단위로 관심사의 분리가 이뤄진다 :

  • TasksProvider : tasks를 다루기
  • useTasks : tasks를 읽어오기
  • useTasksDispatch : 하위 트리 어디서든 tasks 업데이트하기

앱의 사이즈가 커지면서, 이렇게 reducer - context 콤보를 사용하는 경우가 많아질 것이다. 이렇게 하면 상태 끌어올리기를 너무 과도하게 하는 - props drilling을 안하게 될 수 있을 것이다~

3줄요약

  • reducer와 context를 합치면 하위 컴포넌트에서 상위 state를 읽어오고, 수정할 수 있다.
  • Provide 하는법 :
    1. Context 두개 만들기 (state / dispatch함수)
    2. reducer를 사용하는 컴포넌트에서 두 Context 제공 Provide
    3. 필요한 하위 컴포넌트에서 사용
  • 코드 정리 - 한 파일로 합치자 :
    • TasksProvider같은 provider 컴포넌트를 export
    • useTasksuseTasksDispatch같은 custom hook을 export
profile
잡식성 누렁이 개발자

0개의 댓글