🐕 useReducer를 사용하면..
너무 복잡한 state를 다뤄야할 때 유용합니다.
따라서 간단한 상태를 관리할 때에는 useState만으로도 충분하고
굳이 신경쓸 필요없이 익숙한 useState를 써도 좋지만
useState로 관리하려고하면 너무 코드가 길어질 때 쓰면 좋겠네요
useState보다 더 다양한 컴포넌트 상황에 따라 다양한 상태를 다른값으로 업데이트 해주고 싶을 때
사용하는 Hook으로 리듀서는 현재상태와 액션값을 전달받아 새로운 상태를 반환하는 함수입니다.
const reducer = (state, action) {
return { ... } // 불변성을 지키면서 업데이트한 새로운 상태를 반환합니다.
이렇게 쓰면 되는데 state는 뭐 그렇다쳐도 action은 좀 생소합니다.
action 값은 주로 다음과 같이 객체 형태로 이루어져있는데
redux를 사용하지 않는다면 문자열, 숫자를 전달해도 상관 없습니다.
redux를 사용할 때엔 어떤 액션인지 알려주는 type 필드가 꼭 필요하기 때문에
액션 객체를 넘겨줘야만 합니다.
다음은 액션 값의 예제입니다.
{
type : 'increment'
}
useReducer부분의 공식문서 설명은 다음과 같습니다.
다수의 하윗값을 포함하는 복잡한 정적 로직을 만드는 경우는
제가 위에서 예를 든 예시와 같고
다음 state가 이전 state에 의존적인 경우 이건 아직 감이 잘 안오네요
저는 공식문서는 뭔가 좀 알고 봐야 아하모먼트가 오는것같습니다.
우선 이지하게 접근하기 위해 유튜브를 봤습니다.
아래 유튜브를 참고하면서 작성했습니다.
https://www.youtube.com/watch?v=tdORpiegLg0
👻공식문서의 reducer 사용방법
const initialState = {count: 0};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
대부분의 예제가 이런식으로 작성되어 있습니다.
처음보면 당황스럽지만 차근차근 useReducer 부분부터 보면 됩니다.
useReducer는 dispatch 메서드와 현재 state를 반환한다고 하는데
리덕스에 대해 사전지식이 있는걸 전제로 설명을 해주니까 잘 모르겠네요
좀 더 찾아보니 state는 현재 상태 dispatch는 액션을 발생시키는 함수이다.
그래서 dispatch안에 액션값을 넣어주면 리듀서 함수가 호출되는 구조다.
dispatch(action)을 해주면 리듀서 함수가 호출이 된다. AHA
🥶 실제로 사용해본 예제
import React, { useState, useReducer } from 'react';
import { Fragment } from 'react';
const ACTION_TYPES = {
deposit: 'deposit',
withdraw: 'withdraw',
};
const reducer = (state, action) => {
console.log('리듀서가일을해요..!', state, action);
switch (action.type) {
case ACTION_TYPES.deposit:
return state + action.payload;
case ACTION_TYPES.withdraw:
return state - action.payload;
default:
return state;
}
};
const App = () => {
const [number, setNumber] = useState(0);
const [money, dispatch] = useReducer(reducer, 0);
return (
<>
<h2>안녕하세요 유즈리듀서에용</h2>
<p>잔고 {money} 원</p>
<input
type="number"
value={number}
onChange={(e) => setNumber(parseInt(e.target.value))}
step="1000"
/>
<button
onClick={() => {
dispatch({ type: ACTION_TYPES.deposit, payload: number });
}}
>
예금
</button>
<button
onClick={() => {
dispatch({ type: ACTION_TYPES.withdraw, payload: number });
}}
>
출금
</button>
</>
);
};
export default App;
위와 같이 동작하는 간단한 컴포넌트입니다.
예금버튼을 누르면 잔고에 인풋값만큼이 추가되고
출금버튼을 누르면 잔고에 인풋값만큼이 감소하는 것인데 이걸 유즈리듀서로 구현한 것
위와 같이 type값을 설정해주는 것을 통해 여러 상태를 관리 할 수 있으며
가장 큰 장점은 컴포넌트 업데이트 로직을 컴포넌트 바깥으로 빼낼 수 있다는 것이
유즈리듀서의 가장 큰 장점이라고 하는데 이건 아직 와닿지가 않네요
dispatch함수 안에 넘겨주는 인자가 꼭 객체일필요도없고
type값을 변수로 지정해줄 필요도 없지만
type값을 변수로 지정해주면 장점이 많겠죠??
만약에 타입을 수정하고 싶을때 일일히 다 수정하는게 아니라
변수 하나의 값만 바꿔주면 다 적용이 될테니까!!
타입값에 따라 payload로 전달해준 input의 밸류값을 더하거나 빼주는 간단한 로직입니다.
좀 더 심화된 예제를 보겠습니다.
🌞리듀서 심화예제 App.js
import 문같이 기본적으로 해줘야하는 것들은 빼고 코드만 보겠습니다.
우리의 목표는 다음과 같습니다.
출석부를 만들것인데
1. 인풋에 뭔가를 적고 [추가] 버튼을 누르면 아래에 인풋에 적은 내용과 삭제버튼이 함께 추가된다.
2. 추가되어있는 리스트의 수가 총학생수로 표현된다.
3. 삭제버튼을 누르면 눌린 삭제버튼을 갖고 있는 개체는 삭제된다.
4. 이름을 클릭하면 css가 토글되는 기능
즉 간단한 CRUD를 구현하는 것인데 이걸 Reducer로 구현을 해보자 하는 것이죵
const ACTION_STUDENT = {
'add-student': 'add-student',
'delete-student': 'delete-student',
'mark-student': 'mark-student',
};
먼저 타입을 변수에 담아주면 좋을 것 같으니까
컴포넌트 바깥에 선언을 해주겠습니다.
ACTION_STUDENT라는 변수안에 객체로 값들을 담아줬습니다.
이 값들은 각 type 이벤트에 맞춰서 행동을 다르게 하기 위해
switch 문에서 사용할 거에요
const initialState = {
count: 0,
students: [],
};
아까 리듀서에는 initialState 즉 초기값이 필요하다고 했습니다.
따라서 초기값도 만들어줄게용 네이밍은 취향에 맞게 하시면 됩니다.
count에는 학생수를 students에는 학생들의 정보를 담아줄 것입니다.
useReducer의 두번째 인자로 넘겨줄거에요
const App = () => {
const [name, setName] = useState('');
const [studentsInfo, dispatch] = useReducer(reducer, initialState);
이제 App 내부에서 작성합니다.
useState()를 사용해서 인풋밸류를 관리해줄 거니까
useState를 하나 사용해주고
useReducer를 사용할건데 두번째 인자로 넣을 값은 만들어뒀으니 그대로 넣고
첫번째 인자로 넣어줄 함수를 작성해줘야겠습니다. 전 reducer라는 이름의 함수를 만들게요
const reducer = (state, action) => {
switch (action.type) {
case ACTION_STUDENT['add-student']:
const name = action.payload.name;
const newStudent = {
id: Date.now(),
name,
isHere: false,
};
return {
count: state.count + 1,
students: [...state.students, newStudent],
};
case ACTION_STUDENT['delete-student']:
return {
count: state.count - 1,
students: state.students.filter(
(student) => student.id !== action.payload.id,
),
};
case ACTION_STUDENT['mark-student']:
return {
count: state.count,
student: state.student.map((student) => {
if (student.id === action.payload.id) {
return { ...student, isHere: !student.isHere };
}
return student;
}),
};
default:
return state;
}
};
아까 switch 문을 사용하면 좋다고 했습니다.
switch문이 익숙하지 않으신 분들은 if, else, else if문을 사용해 작성해도 무방합니다.
action이 무엇인가? 싶을 수 있는데
코드 맨 윗줄을 보면 우리가 작성한 reducer 함수는 첫번째 인자로 state,
두번째 인자로 action을 받도록 설정해두었습니다.
네이밍은 아무렇게나 해도 상관없지만
dispatch() 함수를 호출한 경우 우리가 만든 reducer 함수가 실행이 되고.
dispatch에 인자로 전달한 값이 reducer함수의 두번째 인자로 전달이 됩니다.
첫번째 인자는 현재 상태를 전달해줍니다.
콘솔로그를 찎어서 보도록하겠습니다.
다만 아직 진행 상 완성이 되지 않았으니 그냥 이런 느낌이다만 보시면 될 것 같아요
state값에는 처음에 전달한 initialValue의 상태가 돌아오고
action 값에는 객체가 담겨있네요
return (
<>
<h1>출석부</h1>
<p>총학생수 {studentsInfo.count}</p>
<input
type="text"
placeholder="이름입력"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<button
onClick={() => {
dispatch({ type: 'add-student', payload: { name } });
}}
>
추가
</button>
{studentsInfo.students.map((student) => {
return (
<Student
key={student.id}
name={student.name}
dispatch={dispatch}
id={student.id}
isHere={student.isHere}
/>
);
})}
HTML 라인을 작성해줍니다.
onChange는 input 안의 값이 변경될 때 실행되는 이벤트리스너..?같은 느낌이에요
onClick은 말안해도 알것같은... onClick 이벤트가 발생하면
() => 익명화살표함수를 실행하는데 이 익명화살표함수는
dispatch() 함수에 타입값과 name을 넘겨줍니다.
name은 input값의 밸류로 계속 onChange 함수를 통해 실시간 업데이트 되고 있죠!
아래 로직은 실제 학생의 정보를 나열하기위한 map입니다.
그렇게 어렵지는 않은 로직인데
studentsInfo(useReducer의 현재 상태값)에 담겨있는
student 프로퍼티에 접근하면 student 프로퍼티의 밸류는 배열이니까
.map이라는 배열메서드를 사용할 수 있습니다.
그리고 각 요소에 접근해서 그 값들을 props로 가지는 컴포넌트를 쭉 나열해주는 거죠!
저 Student 컴포넌트는 후에 만들것입니다.
여기까지하면 App.js에서 작성할 코드는 모두 작성했습니다.
다만 작성을 할 때 리액트는 불변성 유지가 중요하기 때문에...
불변성을 위해 복사를 잘 해주지 않으면 오류를 만난다는 점 기억하면서
코드를 한번만 다시 보고 가겠습니다.
const reducer = (state, action) => {
switch (action.type) { // action.type을 기준으로
case ACTION_STUDENT['add-student']: // ACTION_STUDENT의 키값 add-student와 같다면
const name = action.payload.name;
// name에 밸류값을 할당해주고(disPatch()함수에 payload로 넘겨줬죠?)
const newStudent = {
id: Date.now(),
name,
isHere: false,
};
// 값을 업데이트해주기 위해 return값을 설정합니다.
return {
count: state.count + 1,
//학생수를 +1 해줍니다.
students: [...state.students, newStudent],
};
// 자기 자신을 얕은복사 해온뒤 newStudent를 추가로 넣어 배열로 전달합니다.
case ACTION_STUDENT['delete-student']:
return {
count: state.count - 1,
students: state.students.filter(
(student) => student.id !== action.payload.id,
),
};
case ACTION_STUDENT['mark-student']:
return {
count: state.count,
students: state.students.map((student) => {
if (student.id === action.payload.id) {
return { ...student, isHere: !student.isHere };
}
// map 메서드를 통해 id가 일치하는 요소를 찾아 isHere을 반전시켜줍니다.
return student;
}),
};
default:
return state;
}
};
😋student.js
const student = ({ name, dispatch, id, isHere }) => {
return (
<div>
<span
style={{
textDecoration: isHere ? 'line-through' : 'none',
color: isHere ? 'gray' : 'black',
}}
onClick={() => {
dispatch({ type: 'mark-student', payload: { id } });
}}
>
{name}
</span>
<button
onClick={() => {
dispatch({ type: 'delete-student', payload: { id } });
}}
>
삭제
</button>
</div>
);
};
props로 넘겨준 값들을 받아오고
isHere의 상태에 따라 css를 동적으로 결정해줍니다.
onClick이 일어나는 경우 props로 받아온 dispatch함수를 호출해주고요!
🤢토나오는 부분
1. switch 문의 사용 방법이 살짝 익숙하지가 않다.
if문으로 대체해서 쓰는 것도 나쁘지 않을지도...?
2. 아직 어떤 값을 사용하면 좋을지 바로바로 머릿속에서 회전이 안된다.
코드가 이해는 되는데 직접 짜기에는 애매한 수준이다.
좀 더 연습하면서 잘 짤 수 있도록 해봐야겠음
상태를 관리하면서 그것을 복사해서 불변성을 유지하면서
그 변화된 상태를 가지고 화면에다 뿌려주는 작업까지..를 다 물흐르듯이 생각이 안닿는다.
🐶마치며
좀 더 친해지면 쉽게 할 수 있을 것도 같다 화이팅
'react' 카테고리의 다른 글
useCallback을 케이크처럼 쉽게 먹는 방법 (0) | 2023.03.17 |
---|---|
useMemo를 케이크처럼 쉽게 먹는 방법 (0) | 2023.03.13 |
React 프로젝트 시작 전 환경 세팅하기 (0) | 2023.03.01 |
함수형과 클래스형 컴포넌트의 차이를 자세히 설명해주세요 (0) | 2023.02.05 |
리액트 엘리먼트 렌더링이란? (0) | 2023.02.03 |