React

useEffect에 대해 알아보자 👀

yunieyunie 2023. 5. 3. 22:23

다음 코드는 버튼을 누를 때마다 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;