Hook useReducer 는 쉽게 말하면, useState의 상위 호환버전입니다. useReducer를 이해하기 위해서는 먼저 3가지 요소를 이해해야 합니다.
별코딩님께서는 이를 설명하며 은행을 예로 들어서 비유하신다. EDWIN이 은행에 볼 일(State 라고 가정)이 있어서 방문했다고 가정하자. 이때 EDWIN이 할 일은 요구이다. 그리고 은행은 고객의 요청에 대해서 관련된 업무를 수행할 것이다.
여기서 은행에 해당되는 부분이 바로 Redecur의 역할이다. Redecur가 고객의 요청에 따라 State를 수정변경한다고 여기면 될 것 같다.
위의 예시에서 EDWIN이 은행(Redecur)에 요구했던 행위를 바로 dispatch라고 부른다. 보다 정확하게는 요구를 하기 위해 나가는 발걸음으로 여기면 될 것 같다.
그리고 요구의 내용, 가령 "통장을 새로 만들고 싶어요" 또는 "카드를 분실했어요" 와 같은 상황에 대한 설명이 달린 구체적인 언급을 할 것인데, 이를 Action이라고 부를 수 있을 것이다.
import React, { useReducer, useState } from 'react'
function App() {
const [money, dispatch] = useReducer(redecur함수명, 0)
Reducer를 사용하기 위한 준비코드는 위와 같다. 상단에 reducer를 임포트해주고, useReducer로 만들 변수를 생성해 주는 것이다. 첫번째 요소는 money이고 초기값은 보는 것과 같이 0이다. [변수1, 변수2]에서 보듯이 dispatch를 입력하는 공간이 바로 변수2에 해당되며, 대입연산자 오른쪽에서 (매개변수1, 매개변수2)에서 매개변수2는 money의 초기값이라면, 매개변수1은 Redecur 매서드가 실행해줄 함수명을 기록하는 자리이다.
import React, { useReducer, useState } from 'react'
const 리듀스함수 = (state, action) => {
console.log("reducer가 일을 합니다.", state, action)
}
function App() {
const [number, setNumber] = useState(0);
const [money, dispatch] = useReducer(리듀스함수, 0)
// money는 Reducer를 통해서만 가능하며, 변경을 위해서는 dispatch를 사용해야만 가능해진다.
return (
<div>
<h2>useReducer 은행에 오신 것을 환경합니다.</h2>
<p>잔고 : {money}원</p>
<input type="number"
value={number}
onChange={(e => setNumber(parseInt(e.target.value)))}
step="1000"/> {/* inpu:number의 간격을 설정합니다. */}
<button onClick={()=>{dispatch('예금을할께')}}>예금</button>
<button>출금</button>
</div>
)
}
export default App
위의 코드가 수행하는 일을 글로 적어보면 이러하다.
먼저, useReducer에 의해서 money의 초기값은 0으로 설정되어 있다.
둘째, 본문 p태그에서 잔고를 확인할 수 있다.
셋째, 버튼(예금)을 클릭할 때, 콜백함수로 dispath를 실행한다.
넷째, action의 요구에 따라 "리듀스함수"가 일을 한다.
const 리듀스함수 = (state, action)
const [money, dispatch] = useReducer(리듀스함수, 0) 가운데 값에 해당되는 money를 인자로 가져온 것이고,
<button onClick={()=>{dispatch('예금을할께')}}> 에서 dispatch가 가져온 '구체적인 요구사항(action)'을 인자로 가져와서 콘솔에 기록한 것을 볼 수 있다.
import { type } from '@testing-library/user-event/dist/type';
import React, { useReducer, useState } from 'react'
const 리듀스함수 = (state, action) => {
console.log("reducer가 일을 합니다.", state, action)
return state + action.payload
// action으로 input:number에서 onChange로 인해서 setNumber 되어 변경된 useState의 값인 number를
// action.payload로 가져왔다면, 이를 반환할 때 useReducer의 state인 money의 값을 변경할 수 있다.
}
function App() {
const [number, setNumber] = useState(0);
const [money, dispatch] = useReducer(리듀스함수, 0)
// money는 Reducer를 통해서만 가능하며, 변경을 위해서는 dispatch를 사용해야만 가능해진다.
return (
<div>
<h2>useReducer 은행에 오신 것을 환경합니다.</h2>
<p>잔고 : {money}원</p>
<input type="number"
value={number}
onChange={(e => setNumber(parseInt(e.target.value)))}
step="1000"/> {/* inpu:number의 간격을 설정합니다. */}
<button onClick={()=>{dispatch({type:'예금을할께', payload:number})}}>예금</button>
<button>출금</button>
</div>
)
}
export default App
코드 내에도 기록해 두었지만, action으로 input:number에서 onChange로 인해서 setNumber 되어 변경된 useState의 값인 number(action.payload)를 가공해서 반환의 내용에 따라 useReducer의 state인 money의 값을 변경할 수 있다. 이후 렌더링을 통해서 화면을 리프린팅해준다.
즉 action의 type에 따라서 예금과 출금이 수행되어야 할 것이다. 버튼이 두 개이기 때문에, 해당 버튼의 행위를 구분해줄 필요가 있다. 코린이의 관점에서는 아마 조건문으로 바로 action.type === "이것"이면으로 했을 것인데, 별코딩님은 여기서 switch문을 소개해주신다.
const 리듀스함수 = (state, action) => {
console.log("reducer가 일을 합니다.", state, action);
// action으로 input:number에서 onChange로 인해서 setNumber 되어 변경된 useState의 값인 number를
// action.payload로 가져왔다면, 이를 반환할 때 useReducer의 state인 money의 값을 변경할 수 있다.
switch (action.type) {
case '예금할께' :
return state + action.payload;
case '출금할께' :
return state - action.payload;
default:
return state;
}
}
...
function App() {
...
return (</div>
<button onClick={()=>{dispatch({type:'예금할께', payload:number})}}>예금</button>
<button onClick={()=>{dispatch({type:'출금할께', payload:number})}}>출금</button>
</div>
)
}
export default App
콘솔을 보면 type('예금할께')로 들어온 1000원에 따라서 money는 1000원으로 상태를 변경했고, 이후에 들어온 type('출금할께') 2000원에 따라서 money는 마이너스 통장(-1000원)으로 상태가 변경되었다.
만약 리듀서함수에서 설정되지 않은 type이 들어온다면, default 값인 state(money)를 반환해줄 것이다.
const ACTION_KEY = {
예금할께 : '예금할께',
출금할께 : '출금할께'
}
const 리듀스함수 = (state, action) => {
console.log("reducer가 일을 합니다.", state, action);
// action으로 input:number에서 onChange로 인해서 setNumber 되어 변경된 useState의 값인 number를
// action.payload로 가져왔다면, 이를 반환할 때 useReducer의 state인 money의 값을 변경할 수 있다.
switch (action.type) {
case ACTION_KEY.예금할께 :
return state + action.payload;
case ACTION_KEY.출금할께 :
return state - action.payload;
default:
return state;
}
}
...
function App() {
...
return (</div>
<button onClick={()=>{dispatch({type:ACTION_KEY.예금할께, payload:number})}}>예금</button>
<button onClick={()=>{dispatch({type:ACTION_KEY.출금할께, payload:number})}}>출금</button>
</div>
)
}
export default App
결과는 동일하지만, action.type의 내용을 이제 const ACTION_KEY 에서 제어할 수 있게 된 것이다. Wow.
솔직하게 말해서 두번째 실습은 너무 어려웠다. 분명 코드를 보고 치는데, (소괄호)의 유무 {중괄호}의 유무로 인해서 너무나 곤욕스러웠다. 그만큼 문법에 대한 사전이해가 얼마나 중요한 지에 대해서 깨닫는다. 코드는 EDWIN벨로그, 노션(별코딩 강의 코드)에 기록해 두었다.
먼저, 실습예제에 대한 이미지를 먼저 보고 가자. 이미지는 2개인데, 첫번재 이미지는 이름을 추가했을 때, 학생 수가 변화되는 모습을 볼 수 있고,
const reducer = (state, action) => {
switch(action.type) {
case 'add-student' :
const name = action.payload.name;
const newStudent = {id:Date.now(), name, isHere:false}
return {
count:state.count+1,
students:[...state.students, newStudent]
}
default :
return state
}
}
이어지는 이미지에서는 이름을 클릭했을 때 결석을 확인하는 마크, 이름은 회색 그리고 취소선이 발생되는 상태변경에 대한 이미지이다.
case 'mark-student' :
return {
count : state.count,
students : state.students.map(student => {
if(student.id === action.payload.id) {
return {...student, isHere: !student.isHere}
}
return student;
})
}
정리하면, 위의 예제는 2개의 컴포넌트와 4개와 map으로 실행될 컴포넌트 묶음으로 구성된 간단한 구성의 코드이다. 그러나 작성하는 부분은 절대, 결고 간단하지 않았다. 일단 useReducer HOOK에 대해서는 이해했지만, 이를 사용한다는 것이 익숙하지 않고, 생소하기에 어렵기 때문이다.
오늘과 내일 미친듯이 reducer에 대해서 익숙해지자. 심심하면, 수정기능도 꾸준히 연습하고 말이다. 수정도 상당히 어려운 과제였다. useState와 input 태그가 동률로 존재해야 한다는 리액트적 사고에 대해서 이해하고 있지 않으면 작성할 수 없는 부분이기 때문이다. 해당 부분 역시 추후에 글로 남겨 볼 예정이다.
input 태그는 이제 쉽다. useState와 onChange 이벤트를 통해서 제어해주면 되기 때문이다.
const initialState = {
count:0,
students: []
}
해당컴포넌트 밖에 해당 컴포넌트에서 사용할 객체를 선언해준다. 컴포넌트 밖에 사용하는 이유는 추후 useReducer에 해당 내용을 담아서 가공하려 하기 때문이다. 만약 해당컴포넌트 안에 있다면, useReducer에서 사용할 수 없다. useReducer 함수 역시 해당컴포넌트 밖에서 다뤄지기 때문이다.
function App() {
const [name, setName] = useState('')
const [studentsInfo, dispatch] = useReducer(reducer, initialState)
return (...)
}
해당 컴포넌트 안에서 Reducer를 선언한다.
- studentsInfo : 리듀스에서 사용할 식별자(변수명)
- dispatch : JSX부분(return(...))에서 Reducer를 실행할 함수명인데, 이는 리액트에서 정해놓은 이름임으로 변경하지 말자.
- useReducer(reducer : 첫번째 매개변수는 dispatch가 본문에서 호출될 때 실행될 함수이다.
- initialState : studentsInfo에 담겨질 초기값인데, 외부에서 선언한 객체를 불러왔다.
이를 통해서 본문(JSX구문, return())에서 정의된 studentsInfo를 사용할 수 있게 된 것이다.
<p>총 학생 수 : {studentsInfo.count}</p>
위와 같이 말이다. 그리고 입력된 정보에 따라서 해당 숫자는 가변적으로 변경될 것이다. 추가되면 늘어나고, 삭제되면 줄어들 것이다.
<input type="text" value={name} onChange={(e)=> {setName(e.target.value)}}/>
<button onClick={()=>{dispatch({type:'add-student', payload:{name}})}}>추가</button>
input 태그를 통해서 입력된 useState(name)이 버튼(dispatch함수)을 통해서 useReducer(reducer 함수로 전달된다. 이때 전달되는 내용은 action.type과 action.payload 이다.
const reducer = (state, action) => {
switch(action.type) {
case ...
case ...
default :
return state;
}
- state : useReducer의 식별자(studentsInfo)를 가리킨다. 이 부분이 어렵다. 이름이 계속 변경되기 때문이다.
- action : JSX구문에서 dispatch로 받아온 내용을 말하는데, 여기에서는 {객체}로 전달된 자료이다. 객체는 2개의 key를 담고 있다. 하다는 type 이고 하나는 dispatch로 받아올 내용이다.
case 'add-student' :
const name = action.payload.name;
const newStudent = {id:Date.now(), name, isHere:false}
return {
count:state.count+1,
students:[...state.students, newStudent]
}
버튼(dispath:'add-student')을 통해서 받아온 name을 포함한 새로운 객체를 생성하고, 이를 useReducer의 식별자(studentsInfo)에 더해주기 위해서 전개구문을 통하여 깊은복사를 형성함으로 리렌더링을 시켰다.
그리고 이때 state.count+1를 통해서 사람이 추가되었음을 가르쳐주었다.
// jsx부분
<button onClick={()=> {dispatch({type:'delete-student', payload:{id}})}}>삭제</button>
// reducer 함수 부분
case 'delete-student' :
return {
count : state.count -1,
students: state.students.filter(student => student.id !== action.payload.id),
}
filter 배열 매서드를 활용하여, 본문에서 받아온 payload:{id}의 정보를 가지고 useReducer의 식별자(studentsInfo)에서 가공하여, 해당정보를 변경해 주었다.
여기서 깊은복사와 관련된 부분을 하나 다시 각인하고 가고자 한다. 배열매서드 slice, map, filter, reduce 등등은 그 자체로 깊은복사를 지원하며, 원본 배열의 불변성을 지켜준다.
이 기능이 가장 어려웠다. 이는 <span>
태그에 이벤트(onClick)도 달아야 하며, style로 변경해 주어야 하기 때문이다.
// jsx부분
<span style={{
textDecorationLine: isHere ? "line-through" : "none",
color: isHere ? "gray" : "black"}}
onClick={()=> {dispatch({type:"mark-student", payload:{id}})}}>{name}</span>
// reducer 함수 부분
case 'mark-student' :
return {
count : state.count,
students : state.students.map(student => {
if(student.id === action.payload.id) {
return {...student, isHere: !student.isHere}
}
return student;
})
}
버튼(dispatch:"mark-student")을 통해서 해당 요소의 id가 reducer 함수에 전달되고, 해당 case에 따라서 내용이 진행된다.
먼저, 해당 기능은 인원의 변동이 없음으로, state.count를 그대로 유지한다.
둘째(1), 조건문을 통해서 경우의 수를 2개로 나눈다. 하나는 입력된 id와 일치하는 정보이고, 나머지 하나는 그 이외의 경우이다.
둘째(2), 입력된 id와 동일한 정보에 대해서 reducer 함수는 {...student, isHere: !student.isHere}에서 설정한 것과 같이, 객체 내의 정보를 변경한다.
둘째(3), 그리고 일치하지 않는 정보에 대해서는 그대로 반환해줄 것이다.
Editor. EDWIN
date. 23/03/02