본문 바로가기

Client/React.js

<리액트 기초> useReducer

 

· useReducer

* 소개

 - useReducer은 리덕스의 일부 기능들을 사용하게 해준다.

 

 - 비동기 부분 처리를 위해서 리덕스를 써야하지만 reducer와 context로 일부 대체가 가능해졌다.

 

* 사용

 - 틱택토 게임을 만들기 위해서 3x3 표를 만들 것이다.

 

const TicTacToe = () => {

  const [winner, setWinner] = useState('');
  const [turn, setTurn ] = useState('O') ;
  const [tableData, setTableData] = useState([ ['','',''], ['','',''], ['','',''] ]);
  
  //...
}

 

 - 표를 만들기 위해서 2차원배열을 만들었다. 현재 컴포넌트는 TicTacToe > table > tr > td 의 jsx파일들로 만들어져 있다.

 

 - 관리를 좀 더 효율적으로 하기 위해서 위의 state를 줄일 것이다. 이 때 useReducer을 이용하면 하나의 state와 하나의 setState로 통일이 가능해진다.

 

import React, { useState, useReducer, useCallback } from 'react';
import Table from './Table'

const initialState = {
  winner:'',
  turn:'O',
  tableData:[ ['','',''], ['','',''], ['','',''] ],
};

const reducer = (state, action) => {
  //...
};

const TicTacToe = () => {
  const [state, dispatch] = useReducer(reducer, initialState);
  //...
}

 

 - 컴포넌트에는 const [state, dispatch] = useReducer(reducer, initialState) 를 작성한다. 첫 번째 인자는 리듀서이고 두번째 인자는 state이다.

 

 - initialState에는 기존의 state들을 묶어두었다. reducer은 함수이다. 두 개의 매개변수를 받으며, 함수 안에서 state를 어떻게 바꿀지 적게 된다. 이 부분은 아래에서 다뤄보자.

 

 - 리턴문에서 접근은 state.winner와 같이 작성하여 행해진다.

 

 - 예시로 winner의 state값을 O으로 바꾸는 버튼을 만들어보자.

 

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

  
  const onClickTable = useCallback( () => {
    dispatch({ type: 'SET_WINNER', winner: 'O'});
  }, [])
  
  //...
}

 

 - 자식 컴포넌트인 표에 넘겨줘야하므로 useCallback을 사용하여 함수를 넣었다. 이 때 dispatch가 쓰이는데 이 안에 들어가는 것이 action이다. action객체를 만들고 내용을 작성한다. 먼저 type을 임의로 지정하고, 두 번째 원소는 변경될 값을 적어주었다. 이 액션이 dispatch되면 실행될 것이다.

 

 - 액션만 있다고 state가 바뀌진않는다. 이 때 필요한 것이 reducer이다. 아까 만들다말았던 reducer함수를 작성해보자.

 

const SET_WINNER = 'SET_WINNER'

const reducer = (state, action) => {
  switch (action.type) {
    case SET_WINNER:
      // state.winner = action.winner; 이렇게 하면 안됨.
      return {
        ...state,
        winner: action.winner,
      }

  }
};

 

 - 액션이 dispatch될 때마다 reducer가 실행된다. 현재는 액션의 종류가 하나이지만 나중에는 더 늘어날 것이므로 switch문으로 타입별로 구분하여 작성한다. 그렇게 해서 case로 타입을 받고 리턴문으로 아까 작성했던 원소를 받아 state를 변경시킨다. 이 때 state.winner = action.winner와 같이 직접적으로 바꾸면 리액트는 인식을 못한다. 반드시 spread문법으로 얕은 복사를 실행하자.

 

 - 만약 state에 있는 값을 이용해서 변형시킨다면 리턴문에 count : state.count +1 과 같이 사용해야한다. 그게 아니라 통째로 변경이 필요하다면 새로운 변수를 선언하자. 새로운 변수를 state내의 변수와 동일한 이름으로 선언하면 리턴문에 콜론없이 작성가능하다.

 

 - state의 있는 값을 이용해야한다면 if(state.count > 10) 과 같이 state. 을 반드시 써줘야 인식한다. 액션에서 객체로 선언한 프로퍼티도 반드시 action. 을 작성하자.

 

 - 액션의 이름은 변수로 빼두면 따옴표없이 편하게 작성할 수 있다. 액션 이름은 대문자로 하는 것이 관행이다.

 

 - 이제 테이블 컴포넌트에 넘겨주면 테이블에서 클릭함수를 돌릴 수 있다.

 

const TicTacToe = () => {
  const [state, dispatch] = useReducer(reducer, initialState);
  
  const onClickTable = useCallback( () => {
    dispatch({ type: SET_WINNER, winner: '0'});
  }, [])

  return (
    <>
      <Table onClick={onClickTable} tableData={state.tableData}/>
      {state.winner && <div>{state.winner}님의 승리</div>}
    </>
  )
};

 

 - 위와 같이 작성되어 <Table /> 에 onClick과 tableData를 넘겨주었음을 알 수 있다. Table 컴포넌트는 아래와 같이 작성되어 있다.

 

import React from 'react';
import Tr from './Tr'

const Table = ({onClick, tableData}) => {
  return (
    <table onClick={onClick}>
      {Array(tableData.length).fill().map( (tr, i) => (<Tr rowData={tableData[i]} />) ) }
    </table>
    
  )
};

export default Table;

 

 - 표에 클릭함수를 주었고, 내용으로는 3행의 표가 나오도록 하였다.

 

 - 이제 표를 클릭하면 state.winner 가 0이되며, 0님의 승리라는 문구가 웹에 출력된다.

 

 - 정리해보자면 웹에서 발생하는 이벤트는 직접state를 수정할 수 없다. state를 수정하려면 action을 만들고 실행, 즉 dispatch해야 한다. action을 어떻게 처리할지는 reducer에서 관리한다. 이 때 아까 제작한 객체가 action이다. 

 

* 자식컴포넌트와 dispatch

 - 현재 관계 중 가장 아래에 있는 Td.jsx에 dipatch로 액션을 만들고, 메인컴포넌트에서 관리하도록 해보자.

 

  const onClickTd =  useCallback( () => {
    if (cellData) {
      return;
    }
    dispatch({ type: CLICK_CELL, row: rowIndex, cell: cellIndex });

  }, [cellData]);
  
  return (
    <td onClick = {onClickTd}>{cellData}</td>
  )

 

 - 셀에 O 또는 X라는 데이터가 없으면 O또는 X를 출력하기 위해서 별도의 액션을 만들었다.

 

 - cellData는 O또는 X가 될 것이다. 그렇다면 이들을 처리하는 메인컴포넌트의 리듀서를 살펴보자.

 

export const SET_WINNER = 'SET_WINNER';
export const CLICK_CELL = 'CLICK_CELL';
export const CHANGE_TURN = 'CHANGE_TURN';

const reducer = (state, action) => {
  switch (action.type) {
    case SET_WINNER:
      return {
        ...state,
        winner: action.winner,
      }
    case CLICK_CELL: {
      const tableData = [...state.tableData];
      tableData[action.row] = [...tableData[action.row]]; 
      tableData[action.row][action.cell] = state.turn;
      return {
        ...state,
        tableData,
        recentCell: [action.row, action.cell]
      };
    }
    case CHANGE_TURN: {
      return {
        ...state,
        turn: state.turn === 'O' ? 'X' : 'O',
      }
    }
    // ...
}

 

 - 모듈로 만들어야 내보낼 수 있으므로 반드시 export const로 만들어야한다.

 

 - 비동기 문제때문에 기존의 테이블데이터를 얕은 복사하고, td에서 받은 row와 cell을 이용하여 클릭한 셀에 turn값, 즉 O또는 X를 넣어준다.

 

 - 그렇게 새로 만들어진 state와 tableData를 반환하도록 하였고, 가장 최근에 클릭한 행과 열을 기억하도록 recentCell을 만들어둔다.

 

 - CHANGE_TURN은 O 와 X를 번갈아가며 바꾸도록 구현하였다.

 

  return (
    <>
      <Table onClick={onClickTable} tableData={tableData} dispatch={dispatch} />
      {winner && <div>{winner}님의 승리</div>}
    </>
  )

 

 - 메인컴포넌트의 dispatch를 자식컴포넌트로 넘겨줘야하므로 props에 dispatch를 td까지 연달아서 넘겨준다.

 

 

 


 

 

 

참고

 

 

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

 

 

 

 

인프런

 

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

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

www.inflearn.com

 

유튜브

 

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

 

www.youtube.com

 

 

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

<리액트 기초> 리액트 라우터  (0) 2021.03.10
<리액트 기초> Context API  (0) 2021.03.01
<리액트 기초> useMemo & useCallback  (0) 2021.02.24
<리액트 기초> useEffect  (0) 2021.02.22
<리액트 기초> setInterval 사용하기  (0) 2021.02.19