useReducer는 React의 훅(Hook) 중 하나로, 컴포넌트에서 복잡한 상태 로직을 관리할 때 유용하게 사용할 수 있다.
useReducer의 기본 구조
const [state, dispatch] = useReducer(reducer, initialState);
여기서 reducer는 상태 전이 로직을 정의하는 함수이고, initialState는 초기 상태를 의미한다.
reducer 함수
--> reducer 함수는 두 개의 인자를 받는다:
현재 상태 (state)
액션 (action)
const reducer = (state, action) => {
switch (action.type) {
case SET_WINNER:
return {
...state,
winner: action.winner,
};
}
};
switch 문:
SET_WINNER라는 액션 타입만을 처리하고 있다.case SET_WINNER::
...state는 기존 상태를 그대로 복사하는 스프레드 연산자이다.//state.winner = action.winner 처럼 이렇게 바로 바꾸면 안된다.
--> 새로운 객체를 만들어서 바뀐 값만 바꿔주어야 한다!
action 객체
action 객체는 주로 두 개의 속성을 가진다.
type: 어떤 액션인지 명시하는 문자열
payload: 선택적으로, 상태 업데이트에 필요한 데이터
const onClickTable = useCallback(() => {
dispatch({ type: SET_WINNER, winner: 'O' });
}, []);
{ type: SET_WINNER, winner: 'O' }
간단한 사용 예제를 살펴보자
import React, { useReducer } from 'react';
const initialState = { count: 0 };
const reducer = (state, action) => {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
case 'RESET':
return { count: 0 };
default:
return state;
}
};
const Counter = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>Increment</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>Decrement</button>
<button onClick={() => dispatch({ type: 'RESET' })}>Reset</button>
</div>
);
};
export default Counter;
사실 정리를 하려고 하는 지금도 이해가 잘 안된다(?)
그래도 한 번 꾸역꾸역 해보도록 하겠다

이러한 표가 있다.
표를 나타낼 때, table, tr, td 각각의 컨포넌트를 만들어 볼 것이다. (최대한 작게 쪼개는 것이 유용하다)
TicTacToe.jsx

간단하게 기본적인 코드만 먼저 작성해 보았다.
initialState: 게임의 초기 상태를 정의한다.
winner는 현재 승리자를 나타내고, turn은 현재 차례를 나타내며, tableData는 게임 보드의 상태를 나타낸다.
SET_WINNER: 리듀서에서 사용될 액션 타입을 상수로 정의한다. 보통은 이렇게 따로 상수로 정의하는 편이라고 한다.
TicTacToe 컴포넌트: 메인 컴포넌트로, 게임 로직을 처리한다. UseReducer를 사용하여 상태 관리를 하고 있다.
reducer: 리듀서 함수로, 액션에 따라 상태를 업데이트한다. 여기서는 SET_WINNER 액션이 들어오면 winner를 업데이트 한다.
-->
dispatch({ type: SET_WINNER, winner: 'O' })는 SET_WINNER 타입의 액션을 dispatch하여 winner 상태를 'O'로 변경하겠다는 의미를 나타낸다.
onClickTable 콜백 함수 : 테이블을 클릭했을 때 호출되는 함수이다. 여기서는 dispatch 함수를 사용하여 SET_WINNER 액션을 발생시킨다.
그럼 여기서 Table, Tr, Td의 컨포넌트를 작성하러 가보자
Table

틱택토 게임의 게임 보드를 렌더링하는 <Table> 컴포넌트이다.
tableData: 게임 보드의 데이터를 나타내는 배열 --> 각 요소는 셀의 데이터를 나타낸다.
dispatch: 리듀서로 전달되는 액션을 발생시키는 함수이다.
컴포넌트는 <table> 요소를 반환하고, 이어서 <tbody> 요소를 포함한다. 그리고 tableData.length 만큼 반복하는 map 함수를 사용하여 각 행을 나타내는 <Tr> 컴포넌트를 렌더링한다.
--> <Tr> 컴포넌트에는 다음과 같은 props가 전달된다. :
Tr

이 코드는 틱택토 게임에서 한 행을 나타내는 <Tr> 컴포넌트
컴포넌트는 각 행을 나타내는 <tr> 요소를 반환한다. 그리고 rowData.length만큼 반복하는 map 함수를 사용하여 각 셀을 나타내는 <Td> 컴포넌트를 렌더링한다.
각 <Td> 컴포넌트에는 다음과 같은 props가 전달한다:
<Tr> 컴포넌트는 특정 행의 각 셀을 렌더링하고, 각 셀에 대한 데이터를 <Td> 컴포넌트에 전달하여 화면에 표시한다.Td

이 코드는 <Td> 컴포넌트를 정의한다. 이 컴포넌트는 표의 각 셀을 나타낸다.
컴포넌트 내부에서는 onClickTd라는 함수를 정의하고 있다. 이 함수는 사용자가 셀을 클릭했을 때 호출되는 콜백 함수로, 다음과 같은 작업을 수행한다.
그렇지 않다면, dispatch 함수를 사용하여 CLICK_CELL이라는 타입의 액션을 발생시킨다. 이 액션에는 현재 셀의 행과 열 인덱스가 포함된다.
그리고 dispatch에 정의된 내용을 가지고 tictactoe에서 reduce 함수에서 어떻게 액션을 발생시킬지 정의한다.
마지막으로, 셀이 클릭 가능하도록 <td> 엘리먼트를 렌더링하고, 클릭 시 onClickTd 함수가 호출되도록 설정한다. 현재 셀에 표시할 데이터는 cellData prop을 통해 전달된다.
그렇다면 다시 TicTacToe.jsx에 돌아가서 마저 코드를 작성해보자
먼저 td에 작성해 두었던 CLICK_CELL에 대해서 살펴보자
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],
};
}
먼저, 현재의 tableData 배열을 복사하여 변경할 준비를 한다. --> 이는 불변성을 유지하기 위해 필요하다
다음으로, 새로운 tableData를 생성한다. 기존의 tableData 배열에서 해당 액션에 따라 변경할 행을 선택한다. 이를 위해 tableData[action.row]를 복사하여 새로운 배열을 생성한다.
그 다음, 새로운 행(tableData[action.row])에서 액션에 지정된 셀(action.cell)의 값을 현재 플레이어의 표시(state.turn)로 설정한다.
변경된 tableData와 최근에 클릭된 셀의 정보([action.row, action.cell])를 새로운 상태 객체에 포함하여 반환한다.
이제 해야 할 것은 무엇이 있을 까?
화면이 로딩이 된 후,
o, x를 번갈아가면서 게임이 진행이 되고
세개가 연달아 되면 게임이 종료된다.
게임이 종료가 되면 다시 게임이 시작이 된다.
--> 이를 코드로 작성하기 위해서는 useEffect()로 작성해야 한다!
useEffect()
useEffect(() => {
const [row, cell] = recentCell;
if (row < 0) {
return;
}
let win = false;
if (
tableData[row][0] === turn &&
tableData[row][1] === turn &&
tableData[row][2] === turn
) {
win = true;
}
if (
tableData[0][cell] === turn &&
tableData[1][cell] === turn &&
tableData[2][cell] === turn
) {
win = true;
}
if (
tableData[0][0] === turn &&
tableData[1][1] === turn &&
tableData[2][2] === turn
) {
win = true;
}
if (
tableData[0][2] === turn &&
tableData[1][1] === turn &&
tableData[2][0] === turn
) {
win = true;
}
console.log(win, row, cell, tableData, turn);
if (win) {
dispatch({ type: SET_WINNER, winner: turn });
dispatch({ type: RESET_GAME });
} else {
let all = true; // all이 true라면 무승부 라는 뜻
tableData.forEach((row) => {
row.forEach((cell) => {
if (!cell) {
all = false;
}
});
});
if (all) {
dispatch({ type: SET_WINNER, winner: null });
dispatch({ type: RESET_GAME });
} else {
dispatch({ type: CHANGE_TURN });
}
}
}, [recentCell]);
이 useEffect는 게임 상태의 변화를 감지하고, 이에 따라 게임의 승자를 확인하고 다음 단계로 진행한다.
최근에 클릭된 셀의 정보를 가져온다. 만약 최근에 클릭된 셀이 없다면(행과 열이 음수일 경우), 아무 작업도 하지 않고 종료된다.
win이라는 변수를 선언하고 초기값을 false로 설정한다. win은 현재 플레이어가 이겼는지 여부를 나타내는 불리언 값이다.
각 행, 열, 대각선의 상태를 확인하여 현재 플레이어가 해당 라인을 완성했는지 여부를 판단합니다. 이때 win 변수를 true로 설정한다.
win 변수가 true이면(즉, 현재 플레이어가 게임을 이겼다면), dispatch 함수를 사용하여 SET_WINNER 액션을 발생시킨다. 이때 winner에는 현재 플레이어의 정보인 turn을 전달한다. 그리고 RESET_GAME 액션도 발생시켜 게임을 초기화한다.
win 변수가 false인 경우, 즉 승자가 없는 상황이라면 모든 셀이 채워져 있는지 확인합니다. 이를 위해 all이라는 변수를 선언하고 초기값을 true로 설정한다. 그리고 tableData 배열의 모든 요소를 확인하면서 모든 셀이 채워져 있는지 확인한다.
if (!cell)은 해당 cell이 비어 있는지(cell이 빈 문자열이나 null 등 falsy한 값인지) 확인한다.
만약 cell이 비어 있으면, all 변수를 false로 설정한다. 이는 빈 셀이 하나라도 있다는 의미이다.
따라서 모든 셀이 채워져 있지 않으면 무승부가 아닌 상황을 의미한다.
무승부
if (all)는 모든 셀이 채워져 있는지를 확인한다. all은 이전에 tableData 배열을 검사하면서 모든 셀이 채워져 있을 때 true로 설정된다.
all이 true이면, 게임판의 모든 셀이 채워져 있고 승자가 없다는 의미이다. 따라서 이는 무승부 상황이다.
dispatch({ type: SET_WINNER, winner: null }) 는 승자를 null로 설정하는 액션을 발생시킨다. 이 액션은 상태 관리 로직에서 승자가 없음을 나타내도록 한다.
dispatch({ type: RESET_GAME }) 는 게임을 초기화하는 액션을 발생시킨다. 이 액션은 상태를 초기 상태로 리셋하여 게임을 다시 시작할 수 있도록 한다.
else는 all이 false인 경우를 처리한다. 즉, 아직 비어 있는 셀이 하나라도 있다는 의미를 나타낸다.
dispatch({ type: CHANGE_TURN }) 는 턴을 변경하는 액션을 발생시킨다. 이 액션은 현재 플레이어의 턴을 다른 플레이어에게 넘기는 역할을 한다.
그렇다면, 이제 CHANGE_TURN, RESET_GAME 코드를 작성하러 가보자
case CHANGE_TURN: {
return {
...state,
turn: state.turn === 'O' ? 'X' : 'O',
};
}
case RESET_GAME: {
return {
...state,
turn: 'O',
tableData: [
['', '', ''],
['', '', ''],
['', '', ''],
],
recentCell: [-1, -1],
};
}
여기서 알고 넘어가야할 부분
return {
...state,
}
...state는 현재 상태를 복사한다. 이는 새로운 상태 객체를 만들기 위해 기존 상태의 모든 속성을 포함하는 것이다. --> 스프레드 문법!
CHANGE_TURN 액션
turn: state.turn === 'O' ? 'X' : 'O', : turn을 변경한다
turn 이 O라면, X로, X라면, O으로 변경!
RESET_GAME 액션
turn: 'O' : turn을 'O'로 설정--> 게임이 다시 시작할 때 'O' 플레이어가 먼저 시작하도록 한다.
tableData를 모두 빈 문자열로 채워진 3x3 배열로 초기화
recentCell: [-1, -1] : 이는 최근에 선택된 셀을 초기화하여, 게임 시작 시 아직 아무 셀도 선택되지 않았음을 나타낸다.
간단한 게임인 줄 알았지만 .. 정말 생각할 부분도,
특히 3X3을 쪼개야 한다는 점이 어렵기도하고, useReducer를 처음 사용해 보니까
해매기도 한 것 같다!!