예제는 인프런의 제로초, "조현영"님의 강의를 들으면서 공부한 내용입니다.
1. useMemo
const Lotto = () => {
const lottoNumbers = useMemo(()=>getWinNumbers(), []);
const [winNumbers, setWinumbers] = useState(lottoNumbers);
...
}
hooks에서 랜더링이 다시 일어날 때 컴포넌트 함수가 전체 코드가 다시 실행되기 때문에 만약 함수 (컴포넌트)안에서 무거운 함수들이나 다시 실행될 필요가 없는 함수들이 포함된 경우라면 랜더링을 다시하는데 매우 비효율적이다.
useRef: 일반 값을 기억한다.
useMemo는 복잡한 함수의 결과값을 기억한다. 즉 useMemo의 첫 번째 콜백함수가 리턴하는 값을 기억하고 두 번째 매개변수로 배열 안에 지정된 값이 변경될 때까지 그 값을 유지하게 된다.
두 번째 인자인 배열의 값이 변경된다면 역시 첫 번째 인자인 콜백함수가 다시 호출된다.
2. useCallback
함수 자체를 기억한다.
const onClickRedo = useCallback(() => { //초기화
console.log('onClickRedo');
console.log(winNumbers);
setWinNumbers(getWinNumbers());
setWinBalls([]);
setBonus(null);
setRedo(false);
timeouts.current = [];
} , [winNumbers]);
컴포넌트가 재실행되면 함수 코드가 재실행되고 컴포넌트 함수 내부에 선언된 (무거운)함수들이 재실행되는 문제를 useMemo를 사용해 함수의 리턴 값을 기억했다면 useCallback을 사용하면 함수선언 자체를 기억해 함수 선언을 다시 하지 않는다.
즉 onClickRedo라는 함수가 다시 작성되지 않는다.
<useCallback을 꼭 사용해야하는 경우>
어떤 컴포넌트 함수 코드에서 자식 컴포넌트를 불러오는데 그 자식 컴포넌트의 prop의 값으로 함수를 전달하는 경우 그 함수는 반드시 useCallback으로 작성된 함수여야한다. 그 이유는 위 예제와 같이 onClickRedo 함수를 전달한다고 치면
클릭 이벤트로 onClickRedo 메소드가 실행되면 useCallback이 아닌경우라면 onClickRedo 함수내부에서 setState를 하여 재랜더링을 하면 onClickRedo 함수가 다시 작성되는데 그럼 자식 컴포넌트의 prop로 전달한 onClickRedo 가 변경되는데 이는 자식 입장에서는 prop가 매번 변경되는 것으로 받아들여지기 떄문에 자식 입장에서 아무런 porp나 state 변경이 없어 재랜더링을 하지 않아도 되는 상황인데도 prop로 전달받은 함수가 매번 새로운 함수로 전달되니 자식 컴포넌트가 재랜더링이 계속 되기 때문이다.
<배열 값에는 어떤 값이 들어감?>
만약 배열에 아무 값도 주지 않은 상태라 치고 4. 코드를 실행해보며 처음 컴포넌트 함수 코드가 이제 실행되는데 순서가 처음 useMemo에 의해 로또 번호가 추첨되고 그 값이 winNumbers에 저장되었을 것이다.
그리고 useCallback함수는 한번 지정이 되었으면 컴포넌트가 재랜더링되더라도 함수가 다시 선언("평가" "실행")되지 않는다.
따라서 onClickRedo가 호출출되어 setWinnumbers로 state 값이 변경은 되나 console.log(winNumbers)가 한번 처음 작성되었고 변경이 되지 않으므로 이 값이 처음 추첨된 번호가 계속 뜨게 된다.
그럼 useCallback함수가 다시 작성되길 즉 "평가" "실행" 이 다시 시작 되게 하려면 배열에 변경되는 값을 지정하면 되는데 useCallback내부에서 사용되는 프로퍼티(사실 걍 아무 프로퍼티나 가능.. 꼭 콜백함수 내부에서 사용되는 프로퍼티일 필요 없음)가 변경된다면 useCallback이 다시 선언된다.
setState로 winNumbers를 변경하므로 이 값을 배열에 적어주면 된다.
※ useCallback이나 useEffect나 배열에 들어가는 값은 첫 번째 매개변수의 콜백 함수 내부에서 사용되는 프로퍼티 값을 적을 수 있ek. useCallback은 배열에 저장된 프로퍼티가 변경된다면 첫 번재 콜백함수가 다시 선언된다. (호출이 아님)
정리
1. 처음 함수 컴포넌트의 함수 코드가 평가되어 실행된다. onClickRedo가 useCallback으로 한 번 콜백함수가 (첫 매개변수) 선언된다.
2. 만약 배열(두번째 파라미터)이 빈 배열일 때, 랜더링이 다시 일어나면 useCallback의 콜백함수가 다시 선언(호출이 아님)되지 않는다. 따라서 conosle.log(winNumbers); 코드의 winNumbers의 값이 과거에 평가되었을 때 그 값을 그대로 가지고 있는다.
3. 만약 배열(두 번째 파라미터)에 변경이 되는 winNumbers를 선언한 경우, 재랜더링이 일어나 컴포넌트 함수 코드가 다시 실행되면 useCallback은 콜백 함수 내부에서 사용되는 프로퍼티의 값이 변경되는 값이 있고 그 값이 두 번째 인자인 배열에 작성되어있으므로 useCallback함수가 다시 선언 (호출이 아님) 즉 평가가 된다.
<정리>
- 함수 자체를 기억
- 실행 자체가 부담이 되는 함수에 사용
- 자식 컴포넌트에 함수를 전달하는 경우 useCallback이 필수
- ⇒ 넘겨주는 함수는 항상 같기 때문에 리렌더링 방지
- useCallback 에서 쓰이는 state를 두 번째 인자에 넣어주어야 변경을 감지
3. useEffect의 review , useEffect로 업데이터 감지하기
useEffect도 역시 두 번째 인자인 배열이 비어있다면 componentDidMount로만 동작을 하는데
만약 두 번째 인자인 배열이 비어있다면 랜더링이 다시 일어날 때 useEffect의 콜백함수가 재호출되지 않는다.
즉 componentDidUpdate의 기능을 못한다.
만약 useEffect의 두 번쨰 인자인 배열에 useCallback과 같이 변경되는 값이 선언되어 있다면 함수 컴포넌트가 재랜더링되어 함수 코드가 다시 실행 될 때 useEffec의 콜백함수가 호출된다. !!
만약 useEffect의 배열에 작성된 프로퍼티가 변경되지 않고, 다른 코드 부분에서 setState가 되어 재랜더링을 한다면?-> useEffect의 콜백함수가 다시 호출되지 않는다. 그 이유는 useEffect의 두 번째 인자인 배열 작성된 프로퍼티가 변경되지 않았기 떄문이다.
※ 헷갈릴거 정리useMemo, useEffect, useCallback세 함수 모두 인자의 형태가 동일하다 하지만 콜 백함수가 어떻게 쓰이는지가 각각 다르다. (이를 역할에 따른 동작이라고 치면..)하지만 두 번째 인자인 배열에 작성되는 값(프로퍼티)가 변경된다면 세 함수 다 콜백함수가 다시 각 역할에 따른 동작으로 동작하게 된다.
● useMemo의 콜백함수의 동작은 콜백함수의 값을 기억하는 것이다. 두 번째 인자인 배열의 값이 변경되면 콜백함수가 디시 실행되어 새로운 값을 기억한다.
● useEffect의 콜백함수의 동작은 처음 랜더링이 되면 콜백함수가 실행되고 배열에 값이 변경 여부에 따라 리랜더링 도리 때 첫 콜백함수가 다시 호출될지 말지를 결정한다.
● useCallback은 콜 백함수 자체를 기억하고 두 번째 배열의 인자가 변경되면 콜백함수가 다시 선언(실행이 아님) 즉 평가 된다.
4. 코드
Lotto.jsx
import React, { useState, useRef, useEffect, useMemo, useCallback } from 'react';
import Ball from './Ball';
function getWinNumbers() {
console.log('getWinNumbers');
const candidate = Array(45).fill().map((v, i) => i + 1);
const shuffle = [];
while (candidate.length > 0) {
shuffle.push(candidate.splice(Math.floor(Math.random() * candidate.length), 1)[0]);
}
const bonusNumber = shuffle[shuffle.length - 1];
const winNumbers = shuffle.slice(0, 6).sort((p, c) => p - c);
return [...winNumbers, bonusNumber];
}
const Lotto = () => {
const lottoNumbers = useMemo(() => getWinNumbers(), []);
const [winNumbers, setWinNumbers] = useState(lottoNumbers);
const [winBalls, setWinBalls] = useState([]);
const [bonus, setBonus] = useState(null);
const [redo, setRedo] = useState(false);
const timeouts = useRef([]);
useEffect(() => {
console.log('useEffect');
for (let i = 0; i < winNumbers.length - 1; i++) {
timeouts.current[i] = setTimeout(() => {
setWinBalls((prevBalls) => [...prevBalls, winNumbers[i]]);
}, (i + 1) * 1000);
}
timeouts.current[6] = setTimeout(() => {
setBonus(winNumbers[6]);
setRedo(true);
}, 7000);
return () => {
timeouts.current.forEach((v) => {
clearTimeout(v);
});
};
}, [timeouts.current]); // 빈 배열이면 componentDidMount와 동일
// 배열에 요소가 있으면 componentDidMount랑 componentDidUpdate 둘 다 수행
useEffect(() => {
console.log('로또 숫자를 생성합니다.');
}, [winNumbers]);
const onClickRedo = useCallback(() => {
console.log('onClickRedo');
console.log(winNumbers);
setWinNumbers(getWinNumbers());
setWinBalls([]);
setBonus(null);
setRedo(false);
timeouts.current = [];
}, [winNumbers]);
return (
<>
<div>당첨 숫자</div>
<div id="결과창">
{winBalls.map((v) => <Ball key={v} number={v} />)}
</div>
<div>보너스!</div>
{bonus && <Ball number={bonus} onClick={onClickRedo} />}
{redo && <button onClick={onClickRedo}>한 번 더!</button>}
</>
);
};
export default Lotto;
Ball.jsx
import React, { memo } from 'react';
const Ball = memo(({ number }) => {
let background;
if (number <= 10) {
background = 'red';
} else if (number <= 20) {
background = 'orange';
} else if (number <= 30) {
background = 'yellow';
} else if (number <= 40) {
background = 'blue';
} else {
background = 'green';
}
return (
<div className="ball" style={{ background }}>{number}</div>
)
});
export default Ball;
'React 라이브러리' 카테고리의 다른 글
13. Context API (0) | 2021.12.26 |
---|---|
12. 틱택토: useReducer (0) | 2021.12.02 |
10. 가위 바위 보 게임 만들기: 라이프 사이클, 고차함수와 Q&A (0) | 2021.11.18 |
10. hooks란 (0) | 2021.11.13 |
9. 이벤트 (0) | 2021.11.13 |