[react] Context API 사용하기

Grace·2021년 5월 26일
5

react

목록 보기
2/6
post-thumbnail

📌 참고
https://codingbroker.tistory.com/125
https://jcon.tistory.com/176
https://react.vlpt.us/mashup-todolist/02-manage-state.html

혹시나 제 글을 읽게 되신다면!
저는 이번에 context API를 처음 접하게 되었고,
공부하며 정리한 내용이기 때문에,, 참고만 해주세요 :)

Context API란?

리액트 프로젝트에서 특정 함수를 컴포넌트를 거쳐서 전달해주기 위해서는
props를 통해 전달하는 작업이 필요하다.
그렇기 때문에 원하는 위치의 컴포넌트에 프로퍼티를 내리 꽂아 전달하는 과정을 거치는데
이를 prop drilling이라고 한다(더라).

context API를 배우기 전에는, 부모컴포넌트에서부터
props가 실제로 필요한 컴포넌트까지 불필요한 전달을 거쳤어야 했다.
그렇기 때문에 아직 공부하는 과정인 나는
어떤 props가 어디서 어디로 전달되는지 계속해서 확인해야했고,
결국 중간에 전달하는 컴포넌트에서 누락되기도 하는 문제가 발생했다,,

또한 이렇게 n번의 prop drilling이 일어날 때의 문제점은

  • 각 컴포넌트에서의 state 관리가 어려움
  • 전달만 하는데에 props를 적어주다보니 코드가 지저분해짐
  • 불필요한 re-rendering으로 인해 발생하는 성능저하

이런 자잘한 실수를 방지하고, 성능을 아주 조금이라도 높히기 위해서
context API로 불필요한 props 전달을 줄여주고
컴포넌트 안에서 전역적으로 관리하는 것이 좋을 수 있다.

Context 작성

본격적으로 context API를 사용해봅시다!

하나하나 나눠서 보려다 보면 이해하기가 더 복잡해지기 때문에(내가 그랬음)
전체적인 코드에서부터 자잘하게 나눠서 봅시다 -

우선 src디렉토리에 context 디렉토리를 만들고
이 파일 안에 두개의 context를 만들텐데
하나는 상태(state)전용의 context이고, 하나는 디스패치(dispatch)전용의 context이다.
이렇게 두개를 따로 만들게 되면, 낭비되는 렌더링을 방지할 수 있다.

예시로 todolist에서 작성했던 context 파일을 보자면

// context/index.tsx
import { createContext, useReducer, ReactNode } from 'react';

// setting types
type Todo = {
  id: number;
  text: string;
  done: boolean;
}
type Actions = 
| { type: "CREATE", text: string }
| { type: "TOGGLE", id: number}
| { type: "REMOVE", id: number}

// setting initial state
const initialState = [
  {
    id: 1,
    text: 'context 연습중',
    done: true
  },
  {
    id: 2,
    text: 'react도 익히는중',
    done: false
  },
  {
    id: 3,
    text: '거기에 typescript까지',
    done: false
  }
];

// create context
const StateContext = createContext<Todo[] | undefined>(undefined);
const DispatchContext = createContext<Dispatch<Actions> | undefined>(undefined);

// create reducer
const Reducer = (state:Todo[], action:Actions): Todo[] => {
  switch(action.type) {
    case "CREATE" :
      const nextId = Math.max(...state.map(todo => todo.id)) + 1;
      return (
        state.concat({
          id: nextId,
          text: action.text,
          done: false
        });
      );
    case "TOGGLE" :
      return state.map(todo => 
        todo.id === action.id ? {...todo, done: !todo.done} : todo
      );
    case "REMOVE" :
      return state.filter(todo => todo.id !== action.id);
    default : 
      throw new Error("Unhandled action");
  }
  
// create Provider component (High order component)
export function ContextProvider({children}:{children:ReactNode}) {
 const [todos, dispatch] = useReducer(Reducer, initialState);
  return (
    <DispatchContext.Provider value={dispatch}>
      <StateContext.Provider value={todos}>
        {children}
      </StateContext.Provider>
    <DispatchContext.Provider>
  );
}

// create custom hooks for using useContext easily
export function useTodoState() {
  const state = useContext(StateContext);
  if(!state) throw new Error("Provider is not found");
  return state;
  
export function useTodoDispatch() {
  const dispatch = useContext(DispatchContext);
  if(!dispatch) throw new Error("Provider is not found");
  return dispatch;
  

전체적으로는 이런 구조로 이루어져있다.
아마 이게 typescript를 쓰면서 쉽게 사용하기에 제일 기본적인 구조 아닐까 싶다.
위에서부터 코드를 분석(?) 해봅시다,,


1. context 생성을 위해 필요한 것들 불러오기

context API의 장점중에 하나는,
context는 react 패키지 내에 포함되어있기 때문에
따로 라이브러리를 설치할 필요가 없다는 것이다.

import { createContext, useReducer, ReactNode } from 'react';

2. context 생성하기

context는 createContext() 함수를 통해 생성된다.
함수의 파라미터에는 해당 context의 기본값을 설정한다.
이 기본값은 context를 사용할 경우, 값을 따로 지정하지 않으면 사용되는 기본값이다.

const StateContext = createContext<Todo[] | undefined>(undefined);
const DispatchContext = createContext<Dispatch<Actions> | undefined>(undefined);

➔ 타입스크립트에서는 Provider를 사용하지 않을 경우엔
Context의 값이 undefined가 되어야 하기 때문에
위의 코드처럼 context의 값이 TodoState일수도, undefined일수도 있다고 선언해둔다.
그래야만 추후에 StateContext를 사용할 때 타입에러가 나지 않는다.

또한 Action타입을 한번 보자면,

type Action = 
| { type: "CREATE", text: string }
| { type: "TOGGLE", id: number}
| { type: "REMOVE", id: number}

이렇게 타입을 지정해주었는데,
미리 action에 대한 타입을 지정해둔 것이기 때문에

입력할 때에 입력이 가능한 타입들을 이렇게 띄워준다.
그리고 부가적으로 지정해둔 값들 ( text, id )을 사용하지 않게 될 경우에 에러를 뿜어낸다.
타입스크립트는 사용하기 편리하게 해주는 만큼,
내가 작성한 코드에 대한 책임을 져야 한다(?)

3. reducer 함수 작성하기

먼저 reducer란,
현재 상태와 액션 객체를 파라미터로 받아와서 새로운 상태로 반환해주는 함수이다.
기존에 컴포넌트 내에서 상태값 변화를 위해 사용했던
useState에서의 setState와 같이 state 변경함수들을
타입별로 정리해두는 것이다.
쉽게말하면, props로 전달해주는 작업을 위해
부모컴포넌트에 작성했던 상태관련 코드들을 모아둔 것이다.

const Reducer = (state:Todo[], action:Actions): Todo[] => {
  switch(action.type) {
    case "CREATE" :
      const nextId = Math.max(...state.map(todo => todo.id)) + 1;
      return (
        state.concat({
          id: nextId,
          text: action.text,
          done: false
        });
      );
    case "TOGGLE" :
      return state.map(todo => 
        todo.id === action.id ? {...todo, done: !todo.done} : todo
      );
    case "REMOVE" :
      return state.filter(todo => todo.id !== action.id);
    default : 
      throw new Error("Unhandled action");
  }

이 부분이 나는 제일 이해하기가 어려웠는데
물론 자바스크립트 문법들을 익숙하게 다루지 못하는 문제도 있지만,
action을 어떻게 사용해야 하는 것인지 몰랐다.

그런데 여러번 사용하다 보니, 변화가 일어나는 것을 감지하는 부분이라고 생각하니 좀 이해가 됬다.
예를 들면
CREATE타입 부분을 보면

const nextId = Math.max(...state.map(todo => todo.id)) + 1;
      return (
        state.concat({
          id: nextId,
          text: action.text,
          done: false
        });
      );

기존 코드(initialState)와 같이 생각해보면,
nextIdmap함수를 돌렸을 때 {1,2,3}이 나오고
max 함수를 돌리면 결과값이 3이므로, +1을 해줘서 4가 된다.
그래서 기존 state값에 concat 함수로 새로운 값이 추가가 된 배열을 만들 때에

  • id값은 4가 되고
  • text 값은 action 타입에서 지정했을 때 같이 지정해준 text값이 되고
  • done은 새로운 항목이 추가되었을 때에는 체크가 되지 않아야 하므로 false.

나는 이렇게 이해했는데.. 누군가에게 도움이 되면 참 좋겠다 :)

4. Provider 컴포넌트 생성

이 컴포넌트는 context로 상태관리를 해야 하는 컴포넌트들을 감싸는
High order component이다.

export function ContextProvider({children}:{children:ReactNode}) {
 const [todos, dispatch] = useReducer(Reducer, initialState);
  return (
    <DispatchContext.Provider value={dispatch}>
      <StateContext.Provider value={todos}>
        {children}
      </StateContext.Provider>
    <DispatchContext.Provider>
  );
}

여기서 useReducer는 왜 갑자기 사용되었는지?? 어떻게 사용하는거지?? 하는 사람 (나야나)
➔ 우선 useReducer의 사용법은 아래와같다.

const [상태, dispatch함수] = useReducer(reducer함수, 초기상태)

기존에 컴포넌트 내부에서 상태 업데이트를 위해서 useState를 사용했었는데,
useReducer를 사용할 경우 컴포넌트 업데이트 로직을
컴포넌트 외부로 빼낼 수 있게 된다.
이때문에, Provider로 감싼 내부의 컴포넌트들에서는
dispatch함수를 통해 reducer 함수에 접근이 가능하게 되는 것이다.

(추가작업) custom Hooks 생성

위의 작업까지 마치게 되면 우리는 이제 컴포넌트에서
state를 통한 상태값을 사용하거나 dispatch를 통해 변경이 가능해진다.
그런데 이 두 값들을 사용하기 위해서는
컴포넌트 내부에서 useContext를 통해 사용이 가능한데,
타입스크립트를 쓰는 이상, 계속 타입 에러를 신경써야한다.

만약 컴포넌트에서 StateContext를 사용하기 위해서는 아래 작업이 필요하다.

const todos = useContext(StateContext);

그런데 이때, todos의 타입이 이전에 지정해주었던 타입때문에
Todo[] | undefined 일 수가 있게 된다.
따라서 todos(배열)을 사용하기 전에 값이 유효한지 체크가 필요하다.

const todos = useContext(StateContext);
if(!todos) return null;

이렇게 되면 todos를 사용하는 컴포넌트에서 계속 저렇게 체크를 해주어야 하기 때문에
custom Hooks를 만들어서 사용하기 편리하게 만들어주는 것이 좋다.

그렇기 때문에 state와 dispatch에 관한 custom Hooks을 아래와 같이 만들어준다.
( 참고로 export를 해주어야 다른 컴포넌트들에서 사용이 가능함 )

export function useTodoState() {
  const state = useContext(StateContext);
  if(!state) throw new Error("Provider is not found");
  return state;
  
export function useTodoDispatch() {
  const dispatch = useContext(DispatchContext);
  if(!dispatch) throw new Error("Provider is not found");
  return dispatch;

그럼 나중에 사용할 때에는

const todos = useStateContext();
const dispatch = useDispatchContext();

라고 유효성 체크가 필요 없이 쉽게 사용이 가능해진다!


컴포넌트에서 Context 사용하기

이제 지금까지 만든 context를 사용해줄 차례!

먼저 전역적인 상태관리가 필요한 컴포넌트들의 구역을 파악한다.
복잡한 구조의 컴포넌트구조가 아닌 이상
App컴포넌트에서 ContextProvider를 불러와서 기존내용을 감싸준다.

// App.tsx
import React from 'react';
import TodoForm from './components/TodoForm';
import TodoList from './components/TodoList';
import { ContextProvider } from './context/ContextProvider';

function App () {
  return (
    <ContextProvider>
     <TodoForm />
     <TodoList />
    </ContextProvider>
    );
}

export default App;

이렇게 Provider로 감싸주게 되면 이제부터 다른 컴포넌트들에서
자유롭게 statedispatch를 사용할 수 있게 된다.

내가 작업했던 코드를 예로 들어 보자면
submit 작업에서 새로운 항목을 만들어서 전송하는데에
CREATE액션을 사용해보자

function TodoForm() {
  const [ value, setValue ] = useState('');
  const dispatch = useDispatchContext();
  
  const onSubmit = (e:React.FormEvent) => {
    e.preventDefault();
    dispatch({
      type: "CREATE",
      text: value
    });
    setValue('');
  };
  ...

이렇게 dispatch 함수에 타입과 필수값들을 적어주기만 하면
다른 코드 작성 필요 없이 context 파일에 연결되면서
상태관리가 이루어진다.


이전에는 그저 단일컴포넌트 안에서만 상태관리를 하거나
props로 전달하는 작업만 하다가 context API라는 새로운 세계를 접하게 되었다.

처음엔 도저히 이해가 안되서 계속 코드 따라서 작업해보고
다른 작업에 적용해보고, 전체적인 구조를 봤다가 세세히 하나하나 뜯어보기도 하고..
사실 아직도 완벽히 이해했다고는 할 수 없지만
작동법이나 구조에 대해서는 이해를 했고,
덕분에 새로운 편리함을 알게 되었다.

아직 Redux나 MobX를 사용해보려면 멀었지만...
context API를 먼저 접함으로서, 이후에 접할 것들에 대해
이해도가 높아졌으리라 믿는다 :)

profile
쉽게 사는건 재미가 없더군요, 새로 시작합니다🤓

0개의 댓글