컴포넌트 재사용함수형 컴포넌트에게 업데이트 조건을 걸기
App
컴포넌트는 각각 count 와 text 두 개의 state 를 가집니다.counst
state 는 CountView
에게 , text
state 는 TextView
컴포넌트 에게 prop 으로 각각 보내주고 있습니다.App
컴포넌트의 count
state 의 값을 변화 시킬 예정입니다.App
컴포넌트의 count
state 의 값이 바뀌게 되고, state 가 업데이트 되었기 때문에 해당 state 를 가진 App
컴포넌트는 리렌더링이 되게 됩니다.CountView
의 값도 바뀌게 되고, CountView
컴포넌트만 업데이트 될 것 같은 기대를 깨고 두 개의 자식 컴포넌트 TextView
까지 모두 리렌더가 됩니다.이유는 ? 부모 컴포넌트가 리렌더가 되면 자식 컴포넌트들 또한 리렌더가 되기 때문에 TextView
또한 강제로 리런더가 됩니다.
TextView
컴포넌트는 렌더링 될 이유가 없습니다. App
컴포넌트가 바뀌긴 하지만 TextView
컴포넌트가 가지고 있는 prop 은 바뀌는 상황이 아니기 때문입니다.CountView
컴포넌트는 자신이 prop 으로 받는 count
가 바뀔 때만 업데이트 하도록 하고TextView
컴포넌트는 자신이 prop 으로 받는 text
가 바뀔 때만 업데이트 하도록 합니다.count
state 가 변경 되었을 때, TextView
컴포넌트를 업데이트 할 조건이 만족되지 않았기 때문에 TextView
컴포넌트는 리렌더 되지 않고, 연산의 낭비를 막아 성능을 보존 할 수 있습니다.React.memo
입니다.const MyComponent = React.memo(function MyComponent(props) {
/* props를 사용하여 렌더링 */
});
React.memo
는 고차 컴포넌트 입니다고차 컴포넌트는 컴포넌트를 가져와 새 컴포넌트를 반환하는 함수 입니다.출처 : https://ko.reactjs.org/docs/higher-order-components.html
React.memo
는 함수 안에 매개변수로 컴포넌트를 전달하게 되면 더 강화된 새로운 컴포넌트를 반환하게 됩니다.function MyComponent(props) {
감싸주게 되면은 props
가 바뀌지 않으면 리렌더링 하지 않는 강화된 컴포넌트를 돌려줍니다.React.memo
는 부모 컴포넌트에 의한 리렌더를 막아주기 때문 입니다.import { useState, useEffect } from 'react';
const TextView = ({ text }) => {
useEffect(() => {
console.log(`Update :: Text : ${text}`);
});
return <div>{text}</div>;
};
const CountView = ({ count }) => {
useEffect(() => {
console.log(`Update :: Count : ${count}`);
});
return <div>{count}</div>;
};
const OptimizeTest = () => {
const [count, setCount] = useState(1);
const [text, setText] = useState('');
return (
<div style={{ padding: 50 }}>
<div>
<h2>count</h2>
<CountView count={count} />
<button onClick={() => setCount(count + 1)}>+</button>
</div>
<div>
<h2>test</h2>
<TextView text={text} />
<input value={text} onChange={(e) => setText(e.target.value)} />
</div>
</div>
);
};
export default OptimizeTest;
count
를 누르면 부모 컴포넌트인 OptimizeTest
의 state 가 바뀌기 때문에 자식 컴포넌트 CountView
, TextView
는 둘 다 렌더링이 일어나기 때문에 둘 다 console 에 출력이 되는 걸 볼 수 있습니다.const TextView = React.memo(({ text }) => {
useEffect(() => {
console.log(`Update :: Text : ${text}`);
});
return <div>{text}</div>;
});
TextView
컴포넌트는 prop 인 text
가 바뀌지 않으면 절대로 렌더링이 일어나지 않습니다. import React, { useState, useEffect } from 'react';
const CounterA = React.memo(({ count }) => {
useEffect(() => {
console.log(`CountA Update - count : ${count}`);
});
return <div>{count}</div>;
});
const CounterB = React.memo(({ obj }) => {
useEffect(() => {
console.log(`CountB Update - count : ${obj.count}`);
});
return <div>{obj.count}</div>;
});
const OptimizeTest = () => {
const [count, setCount] = useState(1);
const [obj, setObj] = useState({ count: 1 });
return (
<div style={{ padding: 50 }}>
<div>
<h2>Counter A</h2>
<CounterA count={count} />
<button onClick={() => setCount(count)}>A Button</button>
</div>
<div>
<h2>Counter B</h2>
<CounterB obj={obj} />
<button onClick={() => setObj({ count: 1 })}>B Button</button>
</div>
</div>
);
};
export default OptimizeTest;
CounterA
, CounterB
setCount(count)
로 상태변화를 주도 하지만 상태가 바뀔 이유가 없습니다. 1 -> 1 로 바뀌면 변경 됬다고 보기 어렵습니다.React.memo
가 동작을 못하는 것이라고 볼 수 있지만 그렇지 않습니다.obj
가 객체이기 때문입니다.function MyComponent(props) {
/* props를 사용하여 렌더링 */
}
function areEqual(prevProps, nextProps) {
/*
nextProps가 prevProps와 동일한 값을 가지면 true를 반환하고, 그렇지 않다면 false를 반환
*/
}
export default React.memo(MyComponent, areEqual);
React.memo
가 첫 번째 인자 말고도, 두 번째 인자를 받는 것을 볼 수 있습니다.function areEqual
는 prevProps
: 이전의 props 와 nextProps
: 이후의 props 를 받고 동일한 값을 가지면 true 를 반환하고, 그렇지 않다면 false 를 반환 합니다.areEqual
함수에서 깊은 비교를 구현 한다면 정상적으로 동작이 가능합니다.import React, { useState, useEffect } from 'react';
const CounterA = React.memo(({ count }) => {
useEffect(() => {
console.log(`CountA Update - count : ${count}`);
});
return <div>{count}</div>;
});
const CounterB = ({ obj }) => {
useEffect(() => {
console.log(`CountB Update - count : ${obj.count}`);
});
return <div>{obj.count}</div>;
};
const areEqual = (prevProps, nextProps) => {
if(prevProps.obj.count === nextProps.obj.count){
return true // 이전 props 현재 props 가 같다. -> 리렌더링을 일으키지 않게 됩니다.
}
return false;
}
const MemoizedCounterB = React.memo(CounterB, areEqual)
const OptimizeTest = () => {
const [count, setCount] = useState(1);
const [obj, setObj] = useState({ count: 1 });
return (
<div style={{ padding: 50 }}>
<div>
<h2>Counter A</h2>
<CounterA count={count} />
<button onClick={() => setCount(count)}>A Button</button>
</div>
<div>
<h2>Counter B</h2>
<MemoizedCounterB obj={obj} /> // 이 부분
<button onClick={() => setObj({ count: 1 })}>B Button</button>
</div>
</div>
);
};
export default OptimizeTest;
CounterB
함수에 React.memo
를 해제 합니다.const MemoizedCounterB = React.memo(CounterB, areEqual)
작성CoutnerB
함수가 아니라 MemoizedCounterB
함수로 렌더 합니다연산 결과 재사용 하는 방법
const getDiaryAnalysis = () => {
console.log('일기 분석 시작')
const goodCount = data.filter((it)=>it.emotion >= 3).length;
const badCount = data.length - goodCount;
const goodRatio = (goodCount / data.length) * 100;
return {goodCount, badCount, goodRatio}
}
const {goodCount, badCount, goodRatio} = getDiaryAnalysis(); // 함수로 호출한 결과 값을 객체로 반환
getDiaryAnalysis
함수를 선언하여 filter 를 이용하여 emotion
>= 3 이상인 것을 goodCount
에 담았습니다.badCount
는 전체 일기에서 goodCount
를 뺀 수goodRatio
는 전체에서 (goodCount / data.length) * 100;
한 값return (
<div className="App">
<DiaryEditor onCreate={onCreate} />
<div>전체 일기 : {data.length}</div>
<div>기분 좋은 일기 개수 : {goodCount}</div>
<div>기분 나쁜 일기 개수 : {badCount}</div>
<div>기분 좋은 일기 비율 : {goodRatio}</div>
<DiaryList onEdit={onEdit} onRemove={onRemove} diaryList={data} />
</div>
);
console.log('일기 분석 시작')
이 2번 찍혀서 나옵니다.import { useEffect, useRef, useState } from 'react';
import './App.css';
import DiaryEditor from './DiaryEditor';
import DiaryList from './DiaryList';
function App() {
// API 호출 함수
const getData = async () => {
const res = await fetch(
'https://jsonplaceholder.typicode.com/comments'
).then((res) => res.json());
// 가져와서 사용할 데이터 ( 0 ~ 20 )
const initData = res.slice(0, 20).map((it) => {
return {
author: it.email,
content: it.body,
emotion: Math.floor(Math.random() * 5) + 1,
create_date: new Date().getTime(),
id: dataId.current++,
};
});
setData(initData);
};
useEffect(() => {
getData();
}, []);
const [data, setData] = useState([]); // state-배열로 저장할 예정 (리스트)
const dataId = useRef(0);
const onCreate = (author, content, emotion) => {
const create_date = new Date().getTime();
const newItem = {
author,
content,
emotion,
create_date,
id: dataId.current, // 0 을 가리킨다.
};
dataId.current += 1; // id 의 값이 1씩 증가한다.
setData([newItem, ...data]); // 새로운 아이템이 위로 올라오게 하기 위해서 newItem 을 먼저 사용
};
const onRemove = (targetId) => {
const newDiaryList = data.filter((it) => it.id !== targetId);
setData(newDiaryList);
};
const onEdit = (targetId, newContent) => {
setData(
data.map((it) =>
it.id === targetId ? { ...it, content: newContent } : it
)
);
};
const getDiaryAnalysis = useMemo() => {
console.log('일기 분석 시작');
const goodCount = data.filter((it) => it.emotion >= 3).length;
const badCount = data.length - goodCount;
const goodRatio = (goodCount / data.length) * 100;
return { goodCount, badCount, goodRatio };
};
const { goodCount, badCount, goodRatio } = getDiaryAnalysis(); // 함수로 호출한 결과 값을 객체로 반환
return (
<div className="App">
<DiaryEditor onCreate={onCreate} />
<div>전체 일기 : {data.length}</div>
<div>기분 좋은 일기 개수 : {goodCount}</div>
<div>기분 나쁜 일기 개수 : {badCount}</div>
<div>기분 좋은 일기 비율 : {goodRatio}</div>
<DiaryList onEdit={onEdit} onRemove={onRemove} diaryList={data} />
</div>
);
}
export default App;
App
컴포넌트가 첫 Mount 가 될 때, data
state 의 값은 빈 배열이었습니다.const [data, setData] = useState([]);
const {goodCount, badCount, goodRatio} = getDiaryAnalysis();
함수를 한 번 호출하게 됩니다.setData(initData)
가 이루어지게 되면서const getData = async () => {
const res = await fetch(
'https://jsonplaceholder.typicode.com/comments'
).then((res) => res.json());
// 가져와서 사용할 데이터 ( 0 ~ 20 )
const initData = res.slice(0, 20).map((it) => {
return {
author: it.email,
content: it.body,
emotion: Math.floor(Math.random() * 5) + 1,
create_date: new Date().getTime(),
id: dataId.current++,
};
});
setData(initData);
};
data
가 한 번 바뀌게 됩니다.App
컴포넌트가 리렌더가 일어나기 때문에 App
컴포넌트 안의 모든 함수들이 재생성이 되게 되고, 2번의 코드가 다시 수행이 되어 호출 됩니다.data
state 의 상태가 변했기 때문에 App
컴포넌트가 한 번 더 리렌더가 일어납니다. 그렇게 되면 getDiaryAnalysis
는 또 실행이 됩니다.useMemo
를 사용 할 수 있습니다. const getDiaryAnalysis = useMemo(
() => { // 첫 번째 인자
console.log('일기 분석 시작');
const goodCount = data.filter((it) => it.emotion >= 3).length;
const badCount = data.length - goodCount;
const goodRatio = (goodCount / data.length) * 100;
return { goodCount, badCount, goodRatio };
},[data.length] // 두 번째 인자
);
import
를 하고, 최적화 하고 싶은 함수를 감싸주면 됩니다.[data.lenght]
가 변화 할 때 만 콜백함수가 다시 수행하게 됩니다.data
의 state 가 변해도, [data.lenght]
가 변하지 않는 이상 getDiaryAnalysis
는 호출 하지 않습니다.하지만 위의 코드를 그대로 실행하면 오류가 납니다. useMemo로 감싸고 , 의존성 배열을 전달을 해서 함수를 최적화를 하면, 더 이상 함수가 아니게 됩니다!useMemo 기능은 함수를 전달을 받아서 콜백함수가 리턴하는 값을 리턴하게 됩니다.
- 즉,
const getDiaryAnalysis
는 함수가 아니라 값을 리턴 받게 됩니다.
//const { goodCount, badCount, goodRatio } = getDiaryAnalysis(); X
const { goodCount, badCount, goodRatio } = getDiaryAnalysis;
useMemo
를 사용해서 의존성 배열에 어떤 값이 변화할 때만 연산을 다시 실행 할 것인지 명시하게 되면 -> 그 함수를 값 처럼 사용을 해서 연산 최적화를 할 수 있습니다.