1-14. 복잡한 상태 관리 로직 분리하기 - useReducer

밥이·2022년 3월 6일
0

React Project

목록 보기
14/14

복잡한 상태 관리 로직 분리하기 - useReducer

복잡한 상태 로직을 컴포넌트로 부터 분리하기

우리가 만들어보았던 컴포넌트들 중에 가장 복잡하고 많은 상태 업데이트 로직을 가진 컴포넌트는 App 컴포넌트임.
onCreate, onEdit, onRemove 등의 많은 상태변화 함수가 존재함 그리고 이 상태변화 함수들은 컴포넌트 내에만 존재했어야 했고, 이유는 그 상태를 업데이트 하기 위해서 기존의 상태를 참조했어야 했기 때문

여기 보이는 onCreate, onEdit, onRemove 안에 있는 data가 App컴포넌트 안에 있는 data를 가져다 써야하기 때문에 App 컴포넌트 밖에서는 할 수가 없었음

그래서 이런 복잡하고 긴 상태변화 로직을 App컴포넌트 함수 밖으로 분리해보는 기능에 대해서 사용해볼꺼임

컴포넌트에서 상태변화 로직을 분리하자.
useReducer 를 이용하면 상태변화 로직들을 컴포넌트에서 분리할 수 있어서 컴포넌트를 더 가볍게 작성할 수 있도록 도와줌

useReducer의 필요성

useState를 이용하면 상태변화 함수 5개를 각각 모두다 Counter함수 컴포넌트에 작성을 했어야 했음.
상태변화 함수들이 점점 많아지고 복잡해지면 이렇게 무겁게 사용하는건 결코 좋지 않은 방법.

하지만 useReducer 를 이용하면 useState를 대체 할 수 있는 훌륭한 기능임.
const [count, dispatch] = useReducer(reducer, 1); 는
useState를 사용하듯이 배열을 반환하고, 이렇게 비구조 할당을 통해서 사용함.

첫번째로 반환받게 되는 count는 useState에 썼던 것처럼, 그냥 state임, 두번째로 반환받게 되는 dispatch는 상태변화가 일어나는 액션을 발생 시키는 함수 (상태변화를 일으킴)

그 다음에 useReducer() 함수를 호출할때, reducer라는 함수를 꼭 전달 해줘야함

reducer는 dispatch로 일어난 상태변화를 처리해주는 함수임, 그다음 useReducer로 호출한 두번째로 전달하는 1의 값은 count state의 초기값이 됨.

정리 : useReducer()를 사용해, count state를 만들면, 초기값이 1로 할당되고, count의 state값을 변경하고 싶으면, 상태변화 함수인 dispatch를 호출해서 상태변화를 일으키면 상태변화 처리함수인 reducer가 처리를 하게됨.

const reducer = (state, action) => {...}
여기서 첫번째 파라미터 state는 최신의 count state값이되고,
두번째 파라미터 action은 버튼을 클릭할 때 dispatch를 호출하여 전달해줬던 {type:1}의 액션 객체를 전달받게됨. state = 1, action = {type:1}

상태변화를 처리하는 reducer함수에서는 switch케이스를 활용에 action의 type에 따라서 각각 다른걸 반환하게됨
그리고 반환하는 값들은 새로운 state가 됨

정리
add 1 버튼을 클릭해서, reducer를 일으키게됨
type은 1로 전달했으니까 reducer의 action의 type은 case 1: 이니까. state + 1, 즉 1+1을 리턴하게 되서 2는 새로운 상태변화가 됨
그리고 count에 state가 업데이트가 되서 {count}에 반영되어 렌더링됨

dispatch를 호출하면 reducer가 실행되고, 그 reducer가 return하는 값이 count의 값이 된다.

useReducer를 사용하는 이유 : 복잡한 상태변화 로직을 컴포넌트 밖으로 분리하기 위해 사용

useReducer를 활용하여 상태관리로직을 분리해보자

  1. useReducer를 이용하여 useState대체하기

  2. 상태변화를 처리하는 reducer함수 구현하기 (컴포넌트 밖으로 분리)

  3. getData함수 dispatch({type:'INIT' data:initData}) action에 필요한 type과 data를 전달 해줘야함 그러고 setData는 지워도됨

  4. reducer함수에서는
    case:'INIT':
    return action.data -> initData를 리턴한것임
    이 값을 리턴해주면 새로운 data State가 됨

  5. onCreate함수에 dispatch()에 type과 data를 적어주는데 data에는 객체로 묶어서 newItem안에있는 데이터들을 적어서 전달.
    날짜는 reducer에서 따로 만들어서 적용해야하기때문에 빼고 전달
    reducer에서는 newItem 변수 만들고 action으로 받은 data를 스프레드시트로 펼치고, 날짜변수 적어주고 저장
    return 은 배열을 만들어 newItem과 state를 스프레드시트로 써서 리턴.

  6. onRemove, onEdit은 쉬우니까 알아서 하셈

onEdit설명
Edit 타입의 액션이 발생하면. 액션으로는 targetId와 newContent가 전달이 됐었음. 그리고 map함수를 사용하여 targetId와 일치하는 요소가 있으면? 그 요소의 값은 컨테츠만 newContent로 수정해주고, 아니면 : 나머지 요소는 그대로 돌려주고. 이 요소들을 합쳐 새로운 배열을 만들어 새로운 State로 보내줌.

한가지 중요한 사실!!

useReducer를 이용할때 우리가 상태변화를 발생시키는 함수를 dispatch로 썼었는데, 이 dispatch는 함수형 업데이트 이런거 필요없이 그냥 호출만하면 알아서 현재의 State를 reducer함수가 참조를 자동으로 해줌.
useCallback을 사용하면서 [ ]deps를 걱정하지 않아도됨

App.js 코드

import { useCallback, useEffect, useMemo, useReducer, useRef } from 'react';
import './App.css';
import DiaryEditor from './DiaryEditor';
import DiaryList from './DiaryList';
// import OptimizeTest from './OptimizeTest';
// import Lifecycle from './Lifecycle';


const reducer = (state, action) => {
    switch (action.type) {

	case 'INIT': {
		return action.data // API초기값 설정
	}

	case 'CREATE': {
		const create_date = new Date().getTime();
		const newItem = {
			...action.data,
			create_date
		}
		return [newItem, ...state];
	}

	case 'REMOVE': {
		return state.filter((state) => state.id !== action.targetId)
	}

	// EDIT타입의 action이 발생하면 action은 targetId와 newContent를 전달받음
	case 'EDIT': {
		return state.map((state) =>  // map함수를 이용해
			state.id === action.targetId  // targetId와 일치하는 아이템을 찾고
				? { ...state, content: action.newContent } // 그 아이템은 content만 newContent로 수정해주고
				: state																		// 나머지 요소는 그대로 돌려줌
		);	// 이 요소들을 새로운 배열로 만들어 새로운 state로 보내줌
	}

	default:
		return state;
	// state를 그대로 전달해 새로운 값으로 사용한다고 하면은? 그냥 값이 안바뀌도록 state전달
}
}



function App() {

// useState는 이제 사용안하므로 임포트에서 제거
// const [data, setData] = useState([]);

const [data, dispatch] = useReducer(reducer, []);

const dataId = useRef(0);// 어떤 DOM도 선택하지 않고 0이란 값만 들어있음.

// React에서 API호출하여 사용하기
const getData = async () => {
	const res = await fetch('https://jsonplaceholder.typicode.com/comments').then((res) => {
		return res.json();
	})
	// 0~19까지 인덱스를 짜고, map()함수 돌리기
	const initData = res.slice(0, 20).map((res) => {
		return {
			author: res.email,
			content: res.body,
			emotion: Math.floor(Math.random() * 5) + 1, // 0~5까지 랜덤수 추출
			create_date: new Date().getTime(),
			id: dataId.current++ // dataId를 현재 current값으로 넣고나서 ++을 통해 1을 더함
		}
	})
	// reducer는 action객체를 받는데, 타입은 INIT이고, 그 action에 필요한 data는 initData를 전달하여 실행시키겠다
	dispatch({ type: 'INIT', data: initData })

	// API로 가공한 데이터를 일기데이터에 초기값으로 저장
	// setData(initData);
}

// Mount되는 시점에 바로 한번 실행
useEffect(() => {
	// Mount되는 시점에 수행할 콜백함수에 getData()라고 API를 호출하는 함수를 적용 
	getData();
}, [])

const onCreate = useCallback((author, content, emotion) => {

	dispatch({ type: 'CREATE', data: { author, content, emotion, id: dataId.current } })
	// const create_date = new Date().getTime();
	// const newItem = {
	// 	author,
	// 	content,
	// 	emotion,
	// 	create_date,
	// 	id: dataId.current
	// }
	dataId.current += 1; // newItem이 추가될때마다 0번 id는 1씩 증가해야함
	// setData((data) => [newItem, ...data]) // 새로운 일기를 추가하면 제일 위로 올라와야하니까. newItem을 앞으로 설정
}, 													  // ...data는 data state의 객체 전부 펼치기
	[]);
// 항상 최신의 state를 참조할수록 도와주는 함수형 업데이트 -> setData((data) => [newItem, ...data])


// 삭제버튼 클릭시 해당 요소 id를 targetId로 전달받음
const onRemove = useCallback((targetId) => {

	// reducer한테 어떤 id를 가진 일기를 지워, 라고 전달할거기 때문에 targetId를 전달 
	dispatch({ type: 'REMOVE', targetId })

	// console.log(`${targetId}가 삭제되었습니다.`)

	// 원래있던 일기data.id와 삭제버튼을 누른id의 값이 같으면,
	// 그 값은 제외하고 새로운 배열을 만들어서 newDiaryList에 저장
	// const newDiaryList = data.filter((data) => {
	// 	return data.id !== targetId
	// })
	// setData(newDiaryList); // 삭제한 데이터 배열을 setData()에 상태를 변화시킴

	// setData((data) => data.filter((data) => {
	// 	return data.id !== targetId
	// }))
}, []);

// 수정완료 버튼 누를시 실행
// 어떤 id를 가진 일기를 수정할껀지를 targetId로 받고, 
// 어떻게 내용을 변경 시킬건지를 newContent으로 받음
const onEdit = useCallback((targetId, newContent) => {

	dispatch({ type: 'EDIT', targetId, newContent })

	// setData((data) =>
	// 	data.map((data) => {
	// 		return data.id === targetId // 전달한 id값이랑 data에 있는 id값이랑 일치하는 id는 '?' 실행
	// 			? { ...data, content: newContent } // id가 일치하면 수정대상이니 수정한 내용으로 업데이트
	// 			: data // 수정 대상이 아니면 그냥 원래 있던값 리턴
	// 	})
	// );
}, []);

// useMemo() 사용할 때 가장 많이 하는 실수!!
// useMemo()는 어떤 함수를 전달을 받아서, 뭘 반환하냐면 이 콜백함수가 리턴한 "값"을 반환함
// 그러니까 getDiaryAnalysis는 "함수"가 아니고, useMemo로 부터 "값"을 리턴받게됨, 
// 그래서 getDiaryAnalysis 사용할떄는 이렇게 getDiaryAnalysis()함수가 아니라 이렇게 getDiaryAnalysis 값으로 사용해야함
// 그러면 에러 안나고 함수로 호출한 값과 똑같은 값을 얻을 수 가 있음 
const getDiaryAnalysis = useMemo(
	() => {
		// console.log('일기 분석 시작');

		// 기분좋은일기 갯수
		// 일기데이터를 가지고있는 data스테이트에서 filter를 이용해 data의 감정점수가 3이상인것만 새로운 배열로 만들어 길이를 구한 후 반환
		const goodCount = data.filter((data) => {
			return data.emotion >= 3
		}).length;

		// 기분나쁜일기 갯수
		const badCount = data.length - goodCount;

		// 기분좋은일기의 비율
		const goodRatio = (goodCount / data.length) * 100;

		// 기분나쁜일기의 비율
		const badRatio = (badCount / data.length) * 100;

		// 3개의 데이터를 객체로 반환
		return { goodCount, badCount, goodRatio, badRatio }
	}, [data.length] // 일기가 추가되거나, 삭제될때만 실행
);                 // 밑에서 아무리 getDiaryAnalysis()함수를 호출해도 똑같은 리턴은 계산하지 않고 그냥 놔둠 

// 이 함수로 호출한 결과값은 객체로 반환하고 있으니,
// 똑같이 객체로 비구조할당으로 받음
// 3개의 데이터를 얻었으면, 렌더링 하여 사용
const { goodCount, badCount, goodRatio, badRatio } = getDiaryAnalysis;

// useMemo() 정리 
// 어떤 함수가 있고 그 함수가 어떤값을 리턴하고 있는데,
// 그 리턴까지의 연산을 최적화하고 싶다면 useMemo()를 사용해서
// Deps Array에 어떤 값이 변화할때만 이 연산을 다시 수행할것인지를 명시해주면,
// 이 함수를 값처럼 사용해서 연산 최적화를 할 수가 있음.
return (
	<div className="App">
		{/* <OptimizeTest /> */}
		{/* <Lifecycle /> */}
		<DiaryEditor onCreate={onCreate} />
		<div>
			<div>전체일기 : 총 {data.length}개 </div>
			<div>좋은일기 : {goodCount}개</div>
			<div>나쁜일기 : {badCount}개</div>
			<div>좋은일기비율 : {goodRatio}%</div>
			<div>나쁜일기비율 : {badRatio}%</div>
		</div>
		<DiaryList dummyList={data} onRemove={onRemove} onEdit={onEdit} />
	</div>
);
}


export default App;

// onEdit()
// 정리 : setData()를 통해서 어떤 값을 전달 할꺼임
// 	 		 그리고 변경시키는 값을 어떻게 만들꺼냐면 onEdit()이란 함수를 이용할꺼
// 			onEdit() 함수는 특정 일기데이터를 수정하는 함수임
//      targetId를 이제 매개변수로 받은 이 id를 갖는 일기데이터를 배열에서 수정할거기 때문에
//     원본data에 map()함수를 이용하여 모든 요소를 순회하면서 새로운 배열을 만들어서 setData()에 전달함.
// 		 그럼 새로운배열은 수정이완료된 배열을 setData에 전달해야됨.
// 	   모든 요소를 순회하면서 전달 받은 id값이 일치한 id가 있으면 그 요소는 content를 교체하여 수정시키겠다.
//     수정 대상이 아니라면 원래 있던 값을 리턴하겠다.

0개의 댓글