1. component > TestComp.js 생성
import {useState} from "react";
function TestComp() {
const [count, setCount] = useState(0);
const onIncrease = () => {
setCount(count+1);
};
const onDecrease = () => {
setCount(count-1);
};
return (
<div>
<h4>테스트 컴포넌트</h4>
<div>
<bold>{count}</bold>
</div>
<div>
<button onClick={onIncrease}>+</button>
<button onClick={onDecrease}>-</button>
</div>
</div>
);
}
export default TestComp;
2. TestComp를 App 컴포넌트의 자식으로 배치
import TestComp from './component/TestComp'; // TestComp import
return(
<div className='App'>
<TestComp /> //자식으로 설정
(...)
</div>
);
}
export default App;
상태 변화 코드를 컴포넌트에서 분리한다
useState를 이용해 State를 생성하면 상태변화 코드는 컴포넌트 안에 작성해야 한다.
But, useReducer을 사용하면 상태 변화 코드를 컴포넌트 밖으로 분리할 수 있다.
1. TestComp에서 useState로 만든 기능 모두 삭제
function TestComp() {
return (
<div>
<h4>테스트 컴포넌트</h4>
<div>
<bold>0</bold>
</div>
<div>
<button>+</button>
<button>-</button>
</div>
</div>
);
}
export default TestComp;
2. useReducer 활용하기
import { useReducer } from "react"; // react 라이브러에서 import
function reducer() {} //새로운 함수 reducer를 컴포넌트 밖으로 만듦
function TestComp() {
const [count, dispatch] = useReducer(reducer, 0); //useReducer 함수 호출
return (
(...)
);
}
export default TestComp;
const [count, dispatch] = useReducer(reducer, 0)
3. count 를 페이지에 렌더링, 버튼을 눌렀을 때 카운트 늘리거나 줄이기
import { useReducer } from "react";
function reducer() {}
function TestComp() {
const [count, dispatch] = useReducer(reducer, 0);
return (
<div>
<h4>테스트 컴포넌트</h4>
<div>
<bold>{count}</bold> //count 렌더링
</div>
<div>
<button onClick={() => dispatch({type: "INCREASE", data: 1})}>
//<+>버튼을 누르면 함수 dispatch를 호출하고 인수로 객체를 전달한다.
+
</button>
<button onClick={() => dispatch({type: "DECREASE", data: 1})}>
//<->버튼을 누르면 함수 dispatch를 호출하고 인수로 객체를 전달한다.
-
</button>
</div>
</div>
);
}
export default TestComp;
<+> 버튼을 눌렀을때
4. TestComp에서 함수 reducer 작성
function reducer(state, action) {//state : 현재 State 값이 저장, action : dispatch의 action객체 전달
switch(action.type){
case "INCREASE":
return state + action.data;//기존 State에 action객체의 data 값을 더해 반환
case "DECREASE":
return state - action.data;//기존 State에 action객체의 data 값을 빼서 반환
default:
return state;
}
}
5. 새로운 상태변화 추가하기
function reducer(state, action) {
switch(action.type){
case "INIT": //초기화 상태 추가하기
return 0;
(...)
}
}
function TestComp() {
const [count, dispatch] = useReducer(reducer, 0);
return (
<div>
<h4>테스트 컴포넌트</h4>
<div>
<bold>{count}</bold>
</div>
<div>
(...)
<button onClick={() => dispath({type: "INIT"})}> //카운트를 0으로 초기화 하는 버튼 생성
0으로 초기화
</button>
</div>
</div>
);
}
export default TestComp;
import {useRef, useReducer} from "react"; //useReducer import(useState 모두 삭제)
function reducer(state, action) { //state 반환하도록 작성(상태 변화 코드는 찬찬히..)
//상태 변화 코드
return state;
}
function App() {
(...)
const[todo, dispatch] = useReducer(reducer, mockTodo); //useReducer로 대체
return(
(...)
);
}
export default App;
const onCreate = (content) => { //setTodo를 모두 삭제
idRef.current += 1;
};
const onUpdate = (targetId) => {
};
const onDelete = (targetId) => {
};
1. onCreate에서 dispatch 호출, 인수로 할 일 정보를 담은 action 객체 전달하기
const onCreate = (content) => {
dispatch({ //새 할 일 아이템을 생성하기 위한 dispatch 함수 호출
type: "CREATE",
newItem: { // 추가할 할 일 데이터 설정
id: idRef.current,
content,
isDone: false,
createdDate: new Date().getTime(),
},
});
idRef.current += 1;
};
2. reducer에서 action 객체의 type이 CREATE일 때, 새 아이템을 추가하는 상태 변화 코드 작성하기
function reducer(state, action) {
switch(action.type){ //tpye 별로 케이스 나누기
case "CREATE":
return [action.newItem, ...state]; //기존 할 일 아이템에 action객체의 아이템이 추가된 새 배열을 반환한다.
default:
return state;
}
}
<호출 과정>
1. dispatch를 호출하여 인수로 action 객체 전달
2. reducer 함수의 반환값으로 State가 업데이트
3. 할 일 아이템 추가
const onUpdate = (targetId) => {
dispatch({
type: "UPDATE",
targetId, //targetId 프로퍼티에는 체크 여부로 수정할 아이템의 id를 설정
});
};
function reducer(state, action) {
switch(action.type){
case "CREATE":
return [action.newItem, ...state];
case "UPDATE" : {
return state.map((it) => //map 메서드로 순회하면서 매개변수 state에 저장된 아이템 배열에서
it.id === action.targetId //id 값을 비교하여
?{ // 일치하면
...it,
isDone: !it.isDone, // isDone을 토글한 새 배열을 반환한다.
}
: it
);
}
default:
return state;
}
}
const onDelete = (targetId) => {
dispatch({
type: "DELETE",
targetId, //targetId 프로퍼티는 삭제할 아이템의 id 를 설정
});
};
function reducer(state, action) {
switch(action.type){
(...)
case "DELETE": { //filter 메서드로 id와 targetId가 일치하는 할 일 아이템만 제외한 할 일 배열을 반환
return state.filter((it) => it.id !== action.targetId);
}
default:
return state;
}
}
const analyzeTodo = () => { // 새로운 analyzeTodo 작성
const totalCount = todo.length; //todo(State 변수)의 아이템 총 개수
const doneCount = todo.filter((it) => it.Done).length; //완료된 아이템 개수
const notDoneDount = totalCount - doneCount; // 미완료된 아이템 개수
return { //객체에 담아 반환
totalCount,
doneCount,
notDoneDount,
};
};
const {totalCount, doneCount, notDoneDount} = analyzeTodo(); //analyzeTodo 호출하고 반환 객체를 구조분해 할당
(...)
return(
<div className="TodoList">
<h4>TodoList 🌱</h4>
<div>
<div>총개수: {totalCount}</div> //totalCount 렌더링
<div>완료된 할 일: {doneCount}</div>
<div>아직 완료하지 못한 할 일: {notDoneCount}</div>
</div>
(...)
const TodoList = ({ todo, onUpdate, onDelete }) => {
const analyzeTodo = () => {
console.log("analyzeTodo 함수 호출"); // 함수 호출하기
(...)
}
→ 컴포넌트 내부에서 선언한 함수는 렌더링할 때마다 실행된다.
→ State 변수 search가 업데이트 되면서 TodoList 컴포넌트가 리렌더되고, 내부에 선언된 analyzeTodo또한 다시 호출된다.
1. useMemo의 기본 사용법
const value = useMemo(callback, deps);
const value = useMemo(() => { //첫번째 인수로 콜백함수 전달(의존성 배열의 값이 안달라지면 호출X)
return count * count;
}, [count]); //count 값이 변하면 콜백함수를 다시 호출해 변경된 반환값을 value에 저장함.
2. 함수 analyzeTodo의 재호출 방지하기
import { useMemo, useState } from "react"; //useMemo react 라이브러리에서 불러오기
const analyzeTodo = useMemo(() => { //useMemo 호출 후, 첫번째 인수:analyzeTodo 함수 전달
console.log("analyzeTodo 함수 호출");
const totalCount = todo.length;
const doneCount = todo.filter((it) => it.Done).length;
const notDoneCount = totalCount - doneCount;
return {
totalCount,
doneCount,
notDoneCount,
};
}, [todo]); //두번째 인수로 todo (todo값이 변할 때마다 analyzeTodo함수 호출)
const {totalCount, doneCount, notDoneCount} = analyzeTodo;
//useMemo는 함수가 아닌 값을 반환하므로 함수 analyzeTodo에는 값이 저장된다.
// 구조분해 할당의 대상을 기존 analyzeTodo()가 아닌 analyzeTodo로 변경
다음 실습을 위해 "analyzeTodo 함수 호출"을 콘솔에 출력하는 코드는 삭제하기
const CompA = () => {
console.log("컴포넌트가 호출되었습니다."); //횡단 관심사1
returb <div>CompA</div>;
};
const CompB = () => {
console.log("컴포넌트가 호출되었습니다."); //횡단 관심사2
returb <div>CompB</div>;
};
function withLifecycleLogging(WrappedComponent) { //인수로 컴포넌트를 받는다 => 래핑된 컴포넌트
return (props) => {
useEffect(() => {
console.log("Mount!"); // 마운트 출력
return () => console.log("Unmount!"); //언마운트 출력
}, []);
useEffect(() => {
console.log("Update!"); //업데이트 출력
});
return <WrappedComponont {...props} />; //강화된 컴포넌트 반환
};
}
const LifecycleLoggingComponent = withLifecycleLogging(Comp);
Comp : 래핑된 컴포넌트(인수로 받는 컴포넌트)
LifecycleLoggingComponent : 강화된 컴포넌트
withLifecycleLogging : 고차 컴포넌트
1. React.memo 기본 사용법
const memoizedComp = React.memo(Comp);
Comp : 메모이제이션하려는 컴포넌트
const CompA = React.memo(() => {
console.log("컴포넌트가 호출되었습니다.");
return <div>CompA</div>;
});
const Comp = ({a, b, c}) => {
console.log("컴포넌트가 호출되었습니다.");
return <div>Comp</div>;
};
function areEqual(prevProps, nextProps) { //판별함수(preProps는 이전 Props값, nextProps는 새롭게 바뀐 Props값)
if(prevProps.a === nextProps.a) {
return true; //true를 반환하면 리렌더되지 않음
} else {
return false; //false를 반환하면 리렌더됨
}
}
const MemoizedComp = React.memo(Comp, areEqual); //두번째 인수로 판별함수 전달
//Props의 a가 변경될 때만 리렌더 된다.
import React from "react"; //React 라이브러리 추가
const Header = () => {
return (
<div className="Header">
<h3>오늘은 📆</h3>
<h1>{new Date().toDateString()}</h1>
</div>
);
};
export default React.memo(Header); //메모이제이션을 적용하여 내보내기
→ 마운트 할 때만 Header 컴포넌트가 렌더링된다.
import React from "react";
const TodoItem = ({id, content, isDone, createdDate, onUpdate, onDelete}) => {
(...)
};
export default React.memo(TodoItem);
→ 이를 방지하기 위해 useCallback 리액트 훅을 사용한다.
1. useCallback의 기본 용법
const memoizedFunc = useCallback(func, deps)
func : 콜백함수
deps : 의존성 배열
2. useCallback과 함수형 업데이트
const onCreate = useCallback(() => {
setState((state) => [newItem, ...state]);
}, []);
→ setState : 최신 State 값을 추적하기 위한 방법
3. useCallback을 이용해 TodoItem 컴포넌트의 리렌더 방지하기
//App.js
const onUpdate = useCallback((targetId) => { //useCallback으로 수정
dispatch({
type: "UPDATE",
targetId,
});
}, []);
const onDelete = useCallback((targetId) => {
dispatch({
type: "DELETE",
targetId,
});
},[]);
1. 최적화는 항상 마지막에 하기
2. 모든 것을 최적화할 필요는 없다.
3. 컴포넌트 구조를 잘 설계했는지 다시 한번 돌아보기
4. 최적화는 여기서 끝나지 않는다
1. State 값을 변경하는 코드를 (상태 변환 코드)라고 부른다.
2. "상태 변화 코드를 컴포넌트에서 분리한다"는 의미는 상태 변화 코드를 (외부)에 작성한다는 뜻이다.
3. 상태 변화 코드를 컴포넌트 밖으로 분리하기위해 (useReducer) 리액트 훅을 사용한다.
4. dispatch 함수는 인수로 객체를 전달한다 이때 이 객체를 (action 객체)라고 부른다.
5.
const [count, dispatch] = useReducer(reducer, 0)
dispatch함수 호출 → (reducer) 함수 호출 → (count) 값 업데이트
6. 실제 상태 변화는 (
reducer)
함수에서 일어난다.
1. reducer을 상태 변화 함수로 가지고, mockTodo를 초깃값으로 가지는 useReducer을 작성해라.
(상태 변화 촉발 함수 : dispatch, state값 : count)
const mockTodo = [
{
id:0,
isDone: false,
content: "React 공부하기",
createdDate: new Date().getTime(),
},
{
id:1,
isDone: false,
content: "Spring 공부하기",
createdDate: new Date().getTime(),
},
{
id:2,
isDone: false,
content: "Django 공부하기",
createdDate: new Date().getTime(),
},
];
2. onCreate 함수를 참고하여, action.type이 "CREATE"일 때 reducer 함수를 작성하여라.
const onCreate = (content) => {
dispatch({
type: "CREATE",
newItem: {
id: idRef.current,
content,
isDone: false,
createdDate: new Date().getTime(),
},
});
idRef.current += 1;
};
function reducer(state, action) {
switch(action.type){
case "CREATE":
//이 부분을 채우면 됨.
//추가된 아이템 + 원래 state
default:
return state;
}
}
1.
const [count, dispatch] = useReducer(reducer, mockTodo)
2.
function reducer(state, action) {
switch(action.type){
case "CREATE":
return [action.newItem, ...state];
default:
return state;
}
}
출처 : 이정환, 『한 입 크기로 잘라먹는 리액트』, 프로그래밍인사이트(2023), p344-383.
Corner React.js 3
Editor: smurfs
[리액터 스타터3] project 3 [감정 일기장] 만들기 (0) | 2024.01.12 |
---|---|
[리액터 스타터3] 9장 컴포넌트 트리에 데이터 공급하기 (1) | 2024.01.05 |
[리액터 스타터3] project 2. [할 일 관리] 앱 만들기 2 (1) | 2023.12.22 |
[리액터 스타터3] project 2. [할 일 관리] 앱 만들기 1 (1) | 2023.12.01 |
[리액트 스타터3] 8장. hooks (0) | 2023.11.24 |