Today I Learned ... react.js
🙋♂️ React.js Lecture
🙋 My Dev Blog
React Lecture CH 6
1 - 로또 추첨기 컴포넌트
2 - setTimeout 중복 사용
3 - componentDidUpdate
4 - useEffect - 업데이트 감지
5 - useMemo, useCallback
6 - Hooks Tips
// 수정 전
onClickRedo = () => {
this.setState({
winNumbers: getWinNumbers(), // [...winNumbers, bonusNumber]
winBalls: [],
bonus: null,
redo: false,
});
this.runTimeouts();
};
this.timeouts 배열도 빈 배열로 초기화해줌.
(setTimeout 함수들이 담긴 배열)
componentDidUpdate를 이용함.
= 컴포넌트가 업데이트 될때마다 발생함.
-> 조건문을 걸어줘야 버튼을 클릭했을 때만 실행됨.
componentDidUpdate(prevProps, prevState) {
if (this.state.winBalls.length === 0) {
this.runTimeouts();
}
}
버튼 클릭시 -> onClickRedo 실행 -> state 전부 초기화.
초기화 된 state중, winBalls 배열이 빈배열[]이 되어 this.state.winBalls.length === 0
인 상태이므로, 이를 조건식으로 이용한다.
❗️ 주의
- componentDidUpdate는 컴포넌트의 state, props 등이 바뀌어 render()가 다시 실행되는 순간마다 매번 발생한다.
- 따라서, 위와 같이 특정 조건에만 실행하고 싶다면?
-> 조건문을 걸어주자.
import React, { useState, useRef, useEffect } from 'react';
import Ball from './Ball';
const getWinNumbers = () => {
const candidate = Array(45)
.fill()
.map((_, i) => i + 1);
let 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 [winNumbers, setWinNumbers] = useState(getWinNumbers());
const [winBalls, setWinBalls] = useState([]);
const [bonus, setBonus] = useState(null);
const [redo, setRedo] = useState(false);
const timeouts = useRef([]);
// 🔻 이부분을 모르겠다.
useEffect(() => {
runTimeout();
return timeouts.current.forEach((v) => clearTimeout(v));
});
const runTimeout = () => {
for (let i = 0; i < winNumbers.length - 1; i++) {
timeouts.current[i] = setTimeout(() => {
setWinBalls((prevWinBalls) => [...prevWinBalls, winNumbers[i]]);
}, (i + 1) * 1000);
}
timeouts.current[6] = setTimeout(() => {
setBonus(winNumbers[6]);
setRedo(true);
}, 7000);
};
const onClickRedo = () => {
setWinNumbers(getWinNumbers());
setWinBalls([]);
setBonus(null);
setRedo(false);
timeouts.current = [];
runTimeout();
};
return (
<>
<div>당첨 숫자</div>
<div id="결과창">
{winBalls.map((v) => (
<Ball key={v} number={v} />
))}
</div>
<div>보너스!</div>
{bonus && <Ball number={bonus} />}
{redo && <button onClick={onClickRedo}>한번 더!</button>}
</>
);
};
export default Lotto;
useEffect
를 사용한다!useEffect()의 첫번째 인자로 콜백이 전달되고,
두번째 인자로는 dependencies
가 배열로 전달된다.
-> 리액트가 변화를 지켜봐야하는 state.
✅ 만약 deps를 빈 배열[] 로 한다면?
-> 최초 1회만 실행된다. (첫 렌더링시)
=componentDidMount
와 같다!
useEffect(() => {
for (let i = 0; i < winNumbers.length - 1; i++) {
timeouts.current[i] = setTimeout(() => {
setWinBalls((prevWinBalls) => [...prevWinBalls, winNumbers[i]]);
}, (i + 1) * 1000);
}
timeouts.current[6] = setTimeout(() => {
setBonus(winNumbers[6]);
setRedo(true);
}, 7000);
}, []); // deps가 빈배열이면 - componentDidMount 역할
return () => {
// 이부분
}
useEffect(() => {
if (winBalls.length === 0) {
for (let i = 0; i < winNumbers.length - 1; i++) {
timeouts.current[i] = setTimeout(() => {
setWinBalls((prevWinBalls) => [...prevWinBalls, winNumbers[i]]);
}, (i + 1) * 1000);
}
timeouts.current[6] = setTimeout(() => {
setBonus(winNumbers[6]);
setRedo(true);
}, 7000);
return () => {
// componentWillUnmount 역할
timeouts.current.forEach((v) => {
clearTimeout(v);
});
};
}
}, []);
❗️❗️ 주의
deps가 빈 배열이면 -> componentDidMount 역할.
그렇다면, deps에 state가 존재하면? (빈 배열이 X)
-> componentDidUpdate + componentDidMount 수행.
(즉 componentDidUpdate만 수행하는것이 아님.)
useEffect(() => {
for (let i = 0; i < winNumbers.length - 1; i++) {
timeouts.current[i] = setTimeout(() => {
setWinBalls((prevWinBalls) => [...prevWinBalls, winNumbers[i]]);
}, (i + 1) * 1000);
}
timeouts.current[6] = setTimeout(() => {
setBonus(winNumbers[6]);
setRedo(true);
}, 7000);
}, []); // deps가 빈배열이면 - componentDidMount 역할
// 🔻 componentDidUpdate를 예상했음.
useEffect(() => {
if (winBalls.length === 0) {
for (let i = 0; i < winNumbers.length - 1; i++) {
timeouts.current[i] = setTimeout(() => {
setWinBalls((prevWinBalls) => [...prevWinBalls, winNumbers[i]]);
}, (i + 1) * 1000);
}
timeouts.current[6] = setTimeout(() => {
setBonus(winNumbers[6]);
setRedo(true);
}, 7000);
}
}, [winBalls]);
분명 componentDidUpdate
의 기능을 예상하고 작성했지만, 공이 두번씩 렌더링되었다.
즉, deps는 빈 배열이 아니고 state가 들어있지만,
componentDidMount를 수행한 후에 componentDidUpdate를 수행한 것.
(render가 두번 일어남)
즉, useEffect에서는 componentDidMount와 componentDidUpdate를 하나의 useEffect 함수로 구현 가능하다.
-> 어차피 실행하는 코드가 같기 때문에!
그런데, 위 조건대로 코드를 작성하면 오류가 발생한다.
따라서, deps를 수정해주어야 한다.
참고
- deps에는 state만 올 수 있다?
❌ No!- 변경을 관찰할 수 있는 대상이라면 뭐든지 OK.
- 아래 코드에서는 ref로 사용된
timeouts
를 deps로 사용한다.
->timeouts
는 버튼 클릭시 onClickRedo에 의해 빈 배열로 변경됨.❗️주의 - timeouts.current[i] = setTimeout(...) 과 같이 배열의 '요소'가 바뀌는 것은 바뀌는것으로 인지하지 않는다.
useEffect(() => {
if (winBalls.length === 0) {
for (let i = 0; i < winNumbers.length - 1; i++) {
timeouts.current[i] = setTimeout(() => {
setWinBalls((prevWinBalls) => [...prevWinBalls, winNumbers[i]]);
}, (i + 1) * 1000);
}
timeouts.current[6] = setTimeout(() => {
setBonus(winNumbers[6]);
setRedo(true);
}, 7000);
return () => {
// componentWillUnmount 역할
timeouts.current.forEach((v) => {
clearTimeout(v);
});
};
}
}, [timeouts.current]); // ComponentDidMount + ComponentDidUpdate 동시에 수행
🙋♂️ 왜 deps가 바뀐건지?
- 맨 처음에 설정해줬던 조건은
winBalls.length === 0
이였다.- 따라서, deps 배열 안에 [winBalls.length === 0]을 작성해주면?
-> 두번씩 렌더링된다.
- 🙋♀️ 왜 두번씩 렌더링 되는지?
-> winBalls의 초기값이 [] 이므로 맨 처음에도 length === 0 을 만족하게 됨. - 첫 공이 두개 렌더링됨.
winBalls.length === 0
을 적어줬지만,winBalls.length === 0
를 적어주면 오류가 발생한다.getWinNumbers
함수가 계속해서 실행됨.const Lotto = () => { const [winNumbers, setWinNumbers] = useState(getWinNumbers()); ... }
- 위 부분도 매번 다시 실행되므로, getWinNumbers()가 계속해서 실행되는 것임.
⭐️ 이럴 땐,
useMemo
를 이용하자!
-> getWinNumbers가 다시 실행되지 않고, 기억할 수 있게 함.
useMemo(() => {}, [두번째 인자]);
const Lotto = () => {
const lottoNumbers = useMemo(() => getWinNumbers(), []);
// useEffect, useMemo, useCallback은 모두 두번째 인자 []가 존재한다.
const [winNumbers, setWinNumbers] = useState(lottoNumbers);
...
}
이제 콘솔을 살펴보면,
getWinNumbers 함수가 맨 처음 한번만 실행된다.
useMemo는 두번째 인자가 변경되기 전까지는 다시 실행되지 않는다.
참고 - 메모이제이션
- 프로그래밍을 할 때 반복되는 결과를 저장해두고 다음에 같은 결과가 나올 때 빨리 실행함.
- 참고 링크
✅ useMemo vs useRef
useMemo useRef 복잡한 함수 값 기억 일반 값을 기억.
useCallback( () => {}, [use]);
useMemo | useCallback |
---|---|
함수의 리턴값을 기억 | 함수 자체를 기억 |
const onClickRedo = useCallback(() => {
setWinNumbers(getWinNumbers());
setWinBalls([]);
setBonus(null);
setRedo(false);
timeouts.current = [];
}, []);
Q. 그렇다면, 모든 함수에 useCallback을 하는것이 이득인가?
- No. 그렇지만은 않다.
- onClickRedo 함수 안에서 consoel.log(winNumbers)를 해보자.
JSX에서 자식 컴포넌트로 함수를 넘길 때,
그 함수에는 반드시 useCallback을 해줘야한다.const onClickRedo = useCallback(() => {}, []); // JSX (자식 컴포넌트 Button) <Button onClick={onClickRedo}>
- useCallback이 없으면 매번 새로운 함수를 생성하고, 자식 컴포넌트로 계속 새로운 함수를 전달하게 됨.
-> 자식 컴포넌트 입장에서는 계속 새로운 함수가 들어오므로, 매번 렌더링이 된다.
- ❗️ useCallback 안에서
state
를 쓸 때는 항상 e두번째 인자인 'inputs' 배열을 적어준다.
- 까먹을 필요가 있는 경우 (inputs 배열, 즉 두번째 인자에 적어준 값이 바뀌었을때)를 지정해줌.
const onClickRedo = useCallback(() => {
console.log(winNumbers);
setWinNumbers(getWinNumbers());
setWinBalls([]);
setBonus(null);
setRedo(false);
timeouts.current = [];
}, [winNumbers]);
+) useMemo
도 마찬가지이다.
useMemo는 함수의 리턴값을 계속 기억하므로,
const Lotto = () => {
const [winBalls, setWinBalls] = useState([]);
const lottoNumbers = useMemo(() => getWinNumbers(), [winBalls]);
const [winNumbers, setWinNumbers] = useState(lottoNumbers);
...
}
-> 따라서, getWinNumbers 함수가 이전처럼 계속 실행된다. (winBalls가 바뀔때마다)
useEffect | useMemo | useCallback |
---|---|---|
최초 + 두번째 인자가 바뀌면 실행 | 두번째 인자가 바뀌기 전까지 리턴값 기억 | 두번째 인자가 바뀌기 전까지 함수 기억 |
cf. 클래스 컴포넌트에서는?
- componentDidUpdate에서 한번에 가능
-> if문의 조건을 나눠서 코드를 작성해줌.
- 클래스컴포넌트에서는 여러개의 state를 한꺼번에 사용 가능하지만, Hooks에서는 하나의 state당 하나의 useEffect를 쓰되, didMount+didUpdate를 한꺼번에 가능.
예 -
componentDidUpdate(prevProps, prevState) {
if(this.state.winBalls.length === 0) {
// code 1
}
if (prevState.winNumbers !== this.state.winNumbers) {
// code 2
}
useEffect에서 두번째 인자를 빈 배열로 두면
componentDidMount의 역할을 하고,
빈 배열이 아닐 때는 componentDidMount + componentDidUpdate의 역할을 한다.
그렇다면, componentDidUpdate의 역할만 하려면?
-> didMount때 아무것도 안하게 하면 된다.
(일종의 꼼수 사용)
// componentDidUpdate에서만 사용 가능하게
const mounted = useRef(false);
useEffect(() => {
if (!mounted.current) {
mounted.current = true;
} else {
// componentDidUpdate에서 실행할 코드
}
}, [바뀌는 값])
✅ 정리
- useEffect가 mount에도 실행되는건 어쩔 수 없는 것이지만, update시에만 실행하려면 위와 같이 mount시 아무것도 안하게 하면 된다!