다음 코드는 버튼을 누를 때마다 counter가 하나씩 올라가며 콘솔창에 render를 출력한다.
state가 변할 때마다 매번 console.log가 실행된다.
import { useState } from "react";
function App() {
const [counter, setCounter] = useState(0);
const onClick = () => setCounter((prev) => prev + 1);
console.log("render");
return (
<div>
<h1>{counter}</h1>
<button onClick={onClick}>click me</button>
</div>
);
}
export default App;
그런데 만약 api로 데이터를 받아오는 상황이라면 처음 컴포넌트를 실행하여 데이터를 받아온 후에도 state가 변할 때마다 계속 api를 불러오게 된다.
이는 매우 비효율적이다.
따라서 특정 코드들이 첫 컴포넌트 실행에서만 출력되게 하는 useEffect에 대해 알아보자.
useEffect는 두 개의 argument를 가지는 function인데 첫 번째 argument는 한 번만 실행하고 싶은 코드를 넣으면 된다.
여기서 memo와의 차이점에 대해 잠깐 말해보자면,
먼저 memo는 props가 변경되지 않으면 re-render가 되지 않도록 하는 것이고
useEffect는 props가 변경될 때나 컴포넌트의 시작과 끝에서만 실행되게 하는 것이다.
다음과 같이 useEffect를 사용하면 console.log가 처음 한 번만 실행되고 버튼을 여러번 클릭해도 console.log는 더 이상 출력되지 않는 것을 확인할 수 있다.
function App() {
const [counter, setCounter] = useState(0);
const onClick = () => setCounter((prev) => prev + 1);
const onlyOnce = () => {
console.log("render");
};
useEffect(onlyOnce, []);
return (
<div>
<h1>{counter}</h1>
<button onClick={onClick}>click me</button>
</div>
);
}
useEffect 안에 직접 함수를 넣을 수도 있다.
function App() {
const [counter, setCounter] = useState(0);
const onClick = () => setCounter((prev) => prev + 1);
useEffect(() => {
console.log("render");
}, []);
return (
<div>
<h1>{counter}</h1>
<button onClick={onClick}>click me</button>
</div>
);
}
이번엔 검색창을 만들고 검색창에 입력할 때 검색api를 이용한다고 가정해보자.
하지만 버튼을 클릭할 때마다 api를 호출할 필요는 없다.
즉, keyword가 변화할 때만 검색되도록 하고 싶다면 useEffect의 두 번째 argument인 dependencies에 keyword를 넣으면 된다.
function App() {
const [counter, setCounter] = useState(0);
const [keyword, setKeyword] = useState("");
const onClick = () => setCounter((prev) => prev + 1);
const onChange = (event) => setKeyword(event.target.value);
console.log("all render");
useEffect(() => {
console.log("call the api");
}, []);
useEffect(() => {
console.log("SEARCH FOR", keyword);
}, [keyword]);
return (
<div>
<input
value={keyword}
onChange={onChange}
type="text"
placeholder="Search here..."
></input>
<h1>{counter}</h1>
<button onClick={onClick}>click me</button>
</div>
);
}
맨 처음에 all render, call the api, SESARCH FOR 가 출력되고 버튼을 누를 때는 all render 만 콘솔에 출력된다.
그런데 SESARCH FOR는 컴포넌트가 시작될 때도 나오기 때문에 이를 다음과 같이 수정해보자.
function App() {
const [counter, setCounter] = useState(0);
const [keyword, setKeyword] = useState("");
const onClick = () => setCounter((prev) => prev + 1);
const onChange = (event) => setKeyword(event.target.value);
console.log("all render");
useEffect(() => {
console.log("call the api");
}, []);
useEffect(() => {
if (keyword !== "") {
console.log("SEARCH FOR", keyword);
}
}, [keyword]);
return (
<div>
<input
value={keyword}
onChange={onChange}
type="text"
placeholder="Search here..."
></input>
<h1>{counter}</h1>
<button onClick={onClick}>click me</button>
</div>
);
}
그럼 이제 처음엔 all render, call the api 가 출력되고 검색창에 입력을 할 때 SESARCH FOR가 출력된다.
더 쉬운 예제를 통해 마지막으로 정리를 해보자.
function App() {
const [counter, setCounter] = useState(0);
const [keyword, setKeyword] = useState("");
const onClick = () => setCounter((prev) => prev + 1);
const onChange = (event) => setKeyword(event.target.value);
useEffect(() => {
console.log("I run only once.");
}, []);
useEffect(() => {
console.log("I run when counter changes.");
}, [counter]);
useEffect(() => {
console.log("I run when keyword changes.");
}, [keyword]);
useEffect(() => {
console.log("I run when counter and keyword change.");
}, [counter, keyword]);
이 코드를 실행시키면 맨 처음에 모든 문장이 출력된다.
버튼을 클릭할 때 I run when counter changes 와 I run when counter and keyword change 가 출력되고 검색창에 입력할 때 I run when keyword changes와 I run when counter and keyword change 가 출력된다.
useEffect는 리액트가 동작하는 관점에서 보면 방어막 같은 존재이다.
리액트는 state가 변하면 컴포넌트가 재실행 되는데, 재실행되지 않아도 되는 부분을 위해 코드 실행 시점을 관리하는 useEffect를 사용하는 것이다.
[최종 정리]
useEffect는 두 개의 argument를 가지는 function인데 첫 번째는 우리가 실행시킬 코드이고 두 번째는 dependencies로 리액트가 watch하는 것이다.
그래서 dependency가 변화할 때 코드가 실행된다.
dependency가 없어 그냥 빈 array이라면 리액트가 지겨볼(watch) 대상이 없기 때문에 코드가 한 번만 실행된다.
dependency는 array이므로 여러개를 넣을 수도 있다.
이제 마지막으로 useEffect의 기능 중 하나인 Cleanup function에 대해 알아보자.
Cleanup function는 리액트 앱 개발에서 자주 쓰이지는 않지만 알아두는 것이 좋다.
Cleanup function는 컴포넌트가 destroy 될 때도 코드를 실행하게 하는 것이다.
import { useEffect, useState } from "react";
function Hello() {
useEffect(() => {
console.log("create :)");
}, []);
return <h1>hello</h1>;
}
function App() {
const [showing, setShowing] = useState(false);
const onClick = () => setShowing((prev) => !prev);
return (
<div>
{showing ? <Hello /> : null}
<button onClick={onClick}>{showing ? "hide" : "show"}</button>
</div>
);
}
export default App;
위의 코드를 실행시켜보면 show 버튼을 누르면 show가 hide로 바뀌고, h1태그가 삽입되어 hello가 나타나고, 콘솔 창에는 create :)가 나타난다.
다시 hide 버튼을 누르면 삽입된 h1태그는 숨겨지는 것이 아니라 아예 없어지고(destroy) 다시 버튼은 show로 돌아간다.
function Hello() {
useEffect(() => {
console.log("create :)");
return () => console.log("destroy :(");
}, []);
return <h1>hello</h1>;
}
위와 같이 return 으로 실행할 함수를 불러오면된다.
그럼 컴포넌트가 destroy 될 때 destroy :( 가 콘솔에 출력된다.
각 기능을 컴포넌트로 분리한 코드는 다음과 같다.
하지만 보통은 위와 같이 합쳐서 사용한다.
import { useEffect, useState } from "react";
function Hello() {
function destroyFn() {
console.log("destroy :(");
}
function createFn() {
console.log("creat :)");
return destroyFn;
}
useEffect(createFn, []);
return <h1>hello</h1>;
}
function App() {
const [showing, setShowing] = useState(false);
const onClick = () => setShowing((prev) => !prev);
return (
<div>
{showing ? <Hello /> : null}
<button onClick={onClick}>{showing ? "hide" : "show"}</button>
</div>
);
}
export default App;