본문 바로가기

Client/React.js

<리액트 기초> Context API

* 소개

 - 지난 장까지 우리는 부모와 자식관계가 다중관계일 때 props를 물려주기 위해서 한단계씩 아래로 물려주었다. 이는 가독성뿐만 아니라 유지보수를 하기도 번거롭다.

 

 - 위와 같은 상황에서 더 간편하게 props를 물려주게 도와주는 것이 ContextAPI이다.

 

* 사용

 - Form이라는 컴포넌트 파일에 main컴포넌트인 MineSearch에서 dispatch를 넘겨주려고 한다. Form은 다음과 같이 작성되어있다.

 

const Form = () => {
  //...

  const onClickBtn = useCallback( (e) => {
    dispatch({ type: START_GAME, row, cell, mine})
  },[row, cell, mine]);

  return (
    <div>
      <input type="number" placeholder="세로" value={row} onChange={onChangeRow} />
      <input type="number" placeholder="가로" value={cell} onChange={onChangeCell} />
      <input type="number" placeholder="지뢰" value={mine} onChange={onChangeMine} />
      <button onClick={onClickBtn}>시작</button>
    </div>
  )

};

export default Form;

 

 - 기존에는 props로 dispatch를 넘겨줘서 dispatch했었으나 context API를 설정하면 아래에 존재하는 어떠한 컴포넌트에서라도 넘겨 받을 수 있다.

 

 - 현재 main 컴포넌트인 MineSearch를 살펴보자.

 

import React, { useReducer, createContext, useMemo} from 'react';
import Table from './Table';
import Form from './Form';

export const TableContext = createContext({
  tableData: [],
  dispatch: () => {},
});
//...

export const START_GAME = 'START_GAME'

const reducer = (state, action) => {
  switch (action.type){
    case START_GAME:
      return {
        ...state,
        tableData: plantMine(action.row, action.cell, action.mine)
      }
    default:
      return state;
  }
}

const MineSearch = () => {
  const [state, dispatch] = useReducer(reducer, initialState);


  return (
    <TableContext.Provider value={ {tableData: state.tableData, dispatch} }>
      <Form />
      <div>{state.timer}</div>
      <Table />
      <div>{state.result}</div>
    </TableContext.Provider>
  );

};

 

 - 먼저 react.createContext를 불러오고 함수로써 실행한다. 여기에 기본 값을 넣을 수 있는데, 여기서는 큰 의미가 없으므로 형식적인 모양만 맞춰주었다. dispatch는 함수이므로 함수모양으로 작성하였다. 이 때 export const로 작성해야 외부에서 사용가능하다. 액션 타입도 export하여 재사용할 것이다.

 

 - 접근하고 싶은 컴포넌트를 context의 provider로 묶어야한다. 미리 제작했던 context의 Provider를 태그에 넣어주면 된다. 이 때 데이터는 value에 넣어준다. 여기서는 테이블데이터와 dispatch를 넣어주었다.

 

 - 하지만 위의 코드는 수정이 필요하다. 리턴문은 새로 리랜더링 될 때마다 객체가 새로 생기고 contextAPI를 이용하는 자식들도 계속 리랜더링되므로 useMemo로 따로 빼서 캐싱하는 것이 좋다.

 

const MineSearch = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  const value = useMemo( () => ({ tableData: state.tableData, dispatch }), [state.tableData]);

  return (
    <TableContext.Provider value={value}>
      <Form />
      <div>{state.timer}</div>
      <Table />
      <div>{state.result}</div>
    </TableContext.Provider>
  );

};

 

 - useMemo를 사용하여 tableData가 변경될 때에만 재생성되도록하였다.

 

 - 이제 다시 Form 컴포넌트로 돌아가보자.

 

import React, { useState, useCallback, useContext } from 'react';
import {START_GAME, TableContext} from './MineSearch'

const Form = () => {
  //...
  
  const {dispatch} = useContext(TableContext);
  //...
  
  const onClickBtn = useCallback( (e) => {
    dispatch({ type: START_GAME, row, cell, mine})
  },[row, cell, mine]);

 

 - 여기서는 useContext를 사용한다. 그리고 액션타입과 만들어두었던 TableContext도 불러온다.

 

 - const value로 접근하여 value.dispatch로 접근 가능하지만 구조분해를 하여 dispatch로 작성하도록 하였다.

 

 - 타입은 미리 만든 START_GAME으로 작성하고 row, cell, mine 데이터를 액션에 전달하도록 하였다.

 

 - 위의 메인 jsx파일의 코드를 보면 액션을 제작하였음을 알 수 있다. 지뢰를 랜덤하게 넣는 plantMine함수에 각 값을 넣어서 리턴하도록 하였다.

 

 - 이번에는 오른쪽 클릭 시에 일어나는 이벤트를 만들어 보자.

 

  const onRightClickTd = useCallback( (e) => {
    e.preventDefault();
    if (halted) {
      return;
    }
    switch (tableData[rowIndex][cellIndex]) {
      case CODE.NORMAL:
      case CODE.MINE:
        dispatch( {type: FLAG_CELL, row: rowIndex, cell: cellIndex});
        return;
      case CODE.FLAG_MINE:
      case CODE.FLAG:
        dispatch( {type: QUESTION_CELL, row: rowIndex, cell: cellIndex});
        return;
      case CODE.QUESTION_MINE:
      case CODE.QUESTION:
        dispatch({ type: NORMALIZE_CELL, row:rowIndex, cell: cellIndex});
        return;
      default:
        return;
    } 
  }, [tableData[rowIndex][cellIndex], halted]);

  return (
    <td style={getTdStyle(tableData[rowIndex][cellIndex])}
      onClick={onClickTd}
      onContextMenu={onRightClickTd}
    >{getTdText(tableData[rowIndex][cellIndex])}</td>
  )

 

 - 리턴 문을 보면 onContextMenu 이벤트를 통해 오른쪽 클릭을 제어함을 알 수 있다.

 

 - 웹에서 오른쪽 클릭 시 기본적으로 뜨는 메뉴가 안뜨도록 해야 하므로 반드시 e.preventDefault를 넣어준다.

 

 - 데이터 별로 다른 액션을 만들어 주었다. 이 때 바뀌는 값은 데이터이므로 useCallback의 두번째 인자의 배열에 넣어주었다.

 

 - 여기서 액션들을 만들었으면 이제 메인 컴포넌트 파일에서 리듀서로 처리해야한다.

 

export const START_GAME = 'START_GAME';
export const OPEN_CELL = 'OPEN_CELL'
export const CLICK_MINE = 'CLICK_MINE'
export const FLAG_CELL = 'FLAG_CELL'
export const QUESTION_CELL = 'QUESTION_CELL'
export const NORMALIZE_CELL = 'NORMALIZE_CELL'

const reducer = (state, action) => {
  switch (action.type){
    //...
    case CLICK_MINE:{
      const tableData = [...state.tableData];
      tableData[action.row] = [...state.tableData[action.row]];
      tableData[action.row][action.cell] = CODE.CLICKED_MINE;
      return {
        ...state,
        tableData,
        halted: true,
      }
    };
    case FLAG_CELL:{
      const tableData = [...state.tableData];
      tableData[action.row] = [...state.tableData[action.row]];
      if (tableData[action.row][action.cell] === CODE.MINE){
        tableData[action.row][action.cell] = CODE.FLAG_MINE;
      } else {
        tableData[action.row][action.cell] = CODE.FLAG;
      }
      return {
        ...state,
        tableData,
      }
    }
   // ...
    default:
      return state;
  }
}

 

 - 변수들을 export 하는 것을 잊지 말아야 하며, 이것을 받는 곳에서도 import를 잊으면 안된다.

 

 - 귀찮더라도 비동기처리를 위해서 case마다 필요한 데이터는 복제해서 사용하였다.

 

 - 위와 같이 자식컴포넌트에서 액션들을 추상적으로 만들어서 선언하고. 메인이 되는 파일의 리듀서에서 state를 바꾸는 구현은 나중에 세세하게 처리하는 것이 좋다.

 


 

 

참고

 

 

 이 글은 ZeroCho 님의 리액트 무료 강좌를 수강하며 개인적으로 정리하며 쓰는 글입니다.

 

 

 

 

인프런

 

웹 게임을 만들며 배우는 React - 인프런

웹게임을 통해 리액트를 배워봅니다. Class, Hooks를 모두 익히며, Context API와 React Router, 웹팩, 바벨까지 추가로 배웁니다. 초급 웹 개발 프레임워크 및 라이브러리 React 웹 개발 게임개발 온라인 강

www.inflearn.com

 

유튜브

 

리액트 무료 강좌(웹게임)

 

www.youtube.com

 

 

'Client > React.js' 카테고리의 다른 글

<리액트 기초> 동적 라우트 매칭  (0) 2021.03.17
<리액트 기초> 리액트 라우터  (0) 2021.03.10
<리액트 기초> useReducer  (0) 2021.02.26
<리액트 기초> useMemo & useCallback  (0) 2021.02.24
<리액트 기초> useEffect  (0) 2021.02.22