useEffect-완벽가이드-5-2
5-2
흐름을 거슬러 올라가기
컴포넌트 렌더링안의 있는 모든 함수(이벤트 핸들러, 이펙트 등)은 렌더가 호출될 때 정의된 props와 state를 잡아둔다.
아래 두 예제는 같다. props.counter를 하여 props의 참조값을 바라보는 것 같지만 단순히 렌더가 호출될 당시의 prop이다.
function Example(props) {
useEffect(() => {
setTimeout(() => {
console.log(props.counter);
}, 1000);
});
// ...
}
function Example(props) {
const counter = props.counter;
useEffect(() => {
setTimeout(() => {
console.log(counter);
}, 1000);
});
// ...
}
만약 위의 클래스 컴포넌트 동작처럼 최신 값을 이용하고 싶다면 useRef를 통해 상태의 참조값을 바라보면 된댜.
function Example() {
const [count, setCount] = useState(0);
const latestCount = useRef(count);
useEffect(() => {
// 변경 가능한 값을 최신으로 설정한다
latestCount.current = count;
setTimeout(() => {
// 변경 가능한 최신의 값을 읽어 들인다
console.log(`You clicked ${latestCount.current} times`);
}, 3000);
});
// ...
cleanup은 뭐야?
클린업의 목적은 구독과 같은 이펙트를 되돌리는 것이다.
useEffect(() => {
ChatAPI.subscribeToFriendStatus(props.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.id, handleStatusChange);
};
});
첫번째 렌더링에서 prop이 {id: 10} 이고, 두 번쟤 렌더링에서 {id: 20}이라고 하면,
id:20 을 가지고 UI 렌더링
브라우저가 리액트가 던져준 DOM 트리로 그리기 시작. 화면에서 id:20 을 볼 수 있음
id:10에 대한 이펙트 클린업.
id:20을 가지고 이펙트 실행.
라이프사이클이 아닌 동기화
리액트는 prop과 state에 따라 DOM과 동기화한다. 동일하게 이펙트도 리액트 트리 바깥에 있는 것들을 props과 state에 따라 동기화할 수 있다.
리액트에게 이펙트 비교하는 법 가르치기
매 리렌더링마다 DOM 전체를 새로 그리는게 아닌, 리액트가 실제로 바뀐 부분만 DOM을 업데이트한다.
<h1 className="Greeting">
Hello, Dan
</h1>
<h1 className="Greeting">
Hello, Yuzhi
</h1>
const oldProps = {className: 'Greeting', children: 'Hello, Dan'};
const newProps = {className: 'Greeting', children: 'Hello, Yuzhi'};
위 객체를 비교하여 prop의 chilren만 바뀐걸 파악했다. 그래서 아래 코드만 호출된다.
domNode.innerText = 'Hello, Yuzhi';
리액트에게 의존성 거짓말 하지 말기
function SearchResults() {
async function fetchData() {
// ...
}
useEffect(() => {
fetchData();
}, []); // 이게 맞을까요? 항상 그렇진 않지요. 그리고 더 나은 방식으로 코드를 작성하는 방법이 있습니다.
// ...
}
마운트 될 때만 data를 가져온다고 말할 수 있지만, 컴포넌트에 있는 모든값 중 그 이펙트에 사용될 값(props, state, 함수)은 반드시 deps에 있어야한다.
위 코드는 가끔 문제를 일으킨다. 데이터 불러오는 로직이 무한루프에 빠질 수도 있고, 소켓이 너무 자주 반응할 수도 있다. 이 문제를 해결하는 건 의존성을 제거하는게 아니다
의존성 거짓말하면 생기는 일.
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
}
effect가 한번만 실행되야 하므로, deps에 []를 넣는다. 하지만 예제는 숫자가 딱 한 번만 증가한다.
deps는 이펙트를 언제 다시 실행해야하는지 알려줘야할 떄 쓰인다. 그래서 setInterval 한번 실행해놓고 알아서 동작하도록 deps를 []로 작성했다. 근데 왜 문제가 발생할까?
deps의 또다른 정의는 어떤 렌더링 스코프에서 나온 값 중 이펙트에 쓰이는 전부를 알려주는 힌트 라고 인식하면 숫자가 한번 증가하는게 맞다. 하지만 deps는 []라고 정의했기 때문에 이펙트를 절대 다시 실행하지 않고 매 초마다 setCount(0 + 1)만 실행한다.
해결책을 찾아보자.
의존성 솔직하게 적기
두가지 전략이 있다.
이펙트안에서 사용되는 모든 값이 의존성 배열안에 포함되도록 고치기
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, [count]);
count가 변경될 떄마다 인터벌은 해제-재설정을 반복한다. 불필요한 동작이 많다.
이펙트의 코드를 바꿔서 의존성을 적게 넘겨주기
의존성 제거하는 몇가지 기술을 알아보자.
이펙트가 자급자족 하기
일단 count를 뭐땜에 쓰고있는지 알아보자. 오로지 setCount때문에 쓴다. 이 경우 스코프 안에서 count를 사용할 필요가 없다. 이전 상태를 기준으로 업데이트할 수 있는 함수형 업데이터를 전달하면 된다.
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(id);
}, []);
리액트에게 어떻게 상태가 바껴야 하는지 알려줄 수 있는 함수를 보낸다. 위 방법은 의존성을 제거하지 않고도 실제로 문제를 해결할 수 있따는 점을 나타낸다.
qna
Q. 리렌더링 될 떄, 클린업은 언제 일어나는가? A. 브라우저가 리액트가 던져준 DOM 트리를 그리고 나서
Q. 리액트는 이펙트 내의 함수를 매번 재실행하는가? A. deps를 정해주지 않으면 매번 재실행, deps를 정해주면 재실행을 막을 수 있다.
Last updated
Was this helpful?