· 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 님의 리액트 무료 강좌를 수강하며 개인적으로 정리하며 쓰는 글입니다.
인프런
유튜브
'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 |