useEffect가 왜 필요할까?
useEffect 콜백 함수는 언제 실행될까?
cleanup 함수는 뭘까? 🤔
useEffect는 side effect를 처리하기 위해 필요하다. 예를 들어, 특정 컴포넌트가 마운트 된 후 HTTP 요청을 보내야 하거나, 버튼을 눌렀을 때 어떤 값을 localStorage에 저장해야 하거나, 유저의 입력의 유효성을 처리해야 하는 등 다양한 상황에 필요하다.
React의 주기능은 UI를 렌더링하는 것이다. 유저의 클릭이나 입력으로 상태가 달라질 수 있고 보여줘야 하는 UI를 다시 렌더링 할 수 있다.
그러나 이 외에도 처리할 일들이 많다. 서버에서 데이터를 가져오거나, localStorage에 값을 저장하거나, 에러 처리 등 렌더링 외의 것을 side effect 라고 한다.
useEffect의 동작 원리를 파악하기 위해 예시를 들어보자.
만약 MyComponent 라는 컴포넌트가 처음 마운트 될 때 REST API를 통해 데이터를 받아오고 싶다. 이 때 useEffect 훅을 사용하지 않는다면 어떤 일이 발생할까?
import React from 'react';
const MyComponent = () => {
const [data, setData] = useState([]);
const fetchData = async () => {
try {
const response = await axios.get(API_URL);
setData(response);
console.log('FETCH DONE!!');
} catch (error) {
console.log(error);
}
};
fetchData(); // useEffect 사용하지 않음
return (
<div>{data}</div>
);
}
export default MyComponent;
콘솔창이 난리난 것을 확인할 수 있다. 프로그램을 종료하기 전까지 FETCH DONE!!
이 계속해서 찍힌다. 이는 useState 때문인데, HTTP 응답을 받아 setData를 실행하게 되면 data가 포함된 MyComponent가 다시 평가되고 실행되기 때문이다. 즉, 무한 루프에 빠지게 되는 것이다.
import React from 'react';
const MyComponent = () => {
const [data, setData] = useState([]);
const fetchData = async () => {
try {
const response = await axios.get(API_URL);
setData(response);
console.log('FETCH DONE!!');
} catch (error) {
console.log(error);
}
};
useEffect(() => { // useEffect 사용
fetchData();
}, []);
return (
<div>{data}</div>
);
}
export default MyComponent;
이 경우는 FETCH DONE!!
이 한 번만 찍힌다. 즉, MyComponent가 처음 마운트 될 때만 fetchData가 실행된다.
useEffect를 언제 사용해야 하는지, 왜 필요한지에 대해 알아봤으니 어떻게 사용할지 알아보자.
useEffect(callback, [dependency]);
기본적인 문법은 위와 같다. 그러나 dependency 와 callback 함수의 모양에 따라 다르게 동작한다. 각 경우에 따라 callback 함수가 언제 동작하는지 알아보자.
callback 실행 시점
컴포넌트가 처음 마운트 될 때 한 번
useEffect(callback, []);
위 예시에서 사용한 방법이다. dependency 배열이 비어있을 때는 useEffect가 포함된 컴포넌트가 맨 처음 마운트될 때만 callback 함수가 실행된다.
callback 실행 시점
컴포넌트가 처음 마운트 될 때 한 번
dependency가 달라질 때
useEffect(callback, [dependency]);
컴포넌트가 처음 마운트 될 때, 그리고 dependency에 포함된 state나 props 등이 달라질 때 callback 함수가 실행된다.
callback 실행 시점
컴포넌트가 처음 마운트 될 때
컴포넌트가 다시 렌더링 될 때
useEffect(callback);
두 번째 (dependency) 인자가 아예 없는 경우, 컴포넌트가 처음으로 마운트 될 때와 재렌더링 될 때 콜백 함수가 실행된다. useEffect에서 거의 쓰이지 않는 방법이다.
반환하는 함수 실행 시점
effect 실행 전
useEffect(() => {
// effect
return (
// cleanup
);
}, [dependency]);
useEffect의 콜백 함수에 cleanup 함수를 반환하는 경우가 있다. 이 때도 dependency에 따라 cleanup 함수의 동작 시점이 달라진다.
dependency가 빈 배열이 아닐 경우는 다음과 같이 동작한다. 컴포넌트가 처음 마운트 될 때는 effect 부분만 실행된다. 마운트 된 이후에 dependency가 바뀔 때마다 cleanup 함수가 실행되고, 그 후에 effect 가 실행된다.
dependency가 빈 배열일 경우, 컴포넌트가 처음 마운트 될 때 effect가 실행된다. 이후 해당 컴포넌트가 언마운트되면 그 때 cleanup 함수가 실행된다.
입력한 텍스트가 10자 이상인지 유효성 검사하는 예제
onChange
로 감지하고 바뀔 때마다 textChangeHandler
를 실행useEffect
로 text
가 바뀔 때마다 유효성 검사text
가 업데이트 되면, 대기 상태였던 setTimeout
내부 코드를 무효화. 이를 위해 cleanup 함수를 사용하여 기존 setTimeout
을 무효화 함. (clearTimeout
사용)import React, { useEffect, useState } from "react";
import Warn from "./Warn";
export default function App() {
const [text, setText] = useState("");
const [isValid, setIsValid] = useState(true);
const textChangeHandler = (e) => {
setText(e.target.value);
};
useEffect(() => {
let timer;
if(text.length === 0) {
setIsValid(true);
} else {
timer = setTimeout(() => {
console.log("[effect] 유효성 검사중..");
setIsValid(text.trim().length >= 10);
}, 500);
}
return () => {
console.log("[cleanup]");
clearTimeout(timer);
};
}, [text]);
return (
<React.Fragment>
<form>
<textarea
name="text"
value={text}
type="text"
placeholder="10자 이상 작성하세요"
onChange={textChangeHandler}
onBlur={() => setIsValid(true)}
/>
<button type="button">확인</button>
</form>
{!isValid && <Warn />} // 경고 메시지
</React.Fragment>
);
}
아래 콘솔창을 확인해보면 cleanup과 effect가 각각 언제 실행되는지 알 수 있다. cleanup은 입력 이벤트가 발생할 때마다 실행되는 반면, effect (유효성 검사)는 5번 밖에 실행되지 않았다.
왜 cleanup 함수를 반환해야 할까?
만약 return 하는 cleanup 함수가 없었다면 effect 실행 전에 clearTimeout
을 실행할 수 없다. 즉, setTimeout으로 0.5초 후에 실행되기로 한 코드 (유효성 검사) 는 0.5초 후에 실행될 뿐이다. 결국 유효성 검사가 0.5초씩 밀릴 뿐 모든 입력에 대해 유효성 검사를 하게 된다.
이 코드에서 유효성 검사는 매우 간단하지만, 만약 유효성 검사 절차가 까다롭다면 매 입력마다 유효성을 검사하는 방식은 매우 비효율적일 것이다. 따라서 useEffect 작업에서 cleanup 함수를 활용하는 것은 효율적인 작업에 필요하다.