[React] 다시 한번 useEffect를 파헤쳐보자

sohyeon kim·2022년 8월 1일
2

React & Javascript

목록 보기
35/41
post-thumbnail

리액트 hooks 중에서 가장 중요한 useEffect를 파헤쳐보자!!

화면에 처음 렌더링되는 것을 Mount
다시 렌더링이 되는 것을 Update
화면에서 사라진 것을 Unmout

useEffect란 컴포넌트가 렌더링 될 때 특정 작업을 실행할 수 있도록 하는 Hook이다.

  • useEffect는 렌더링 '이후에' 실행이 된다. 즉 컴포넌트가 렌더링이 완료되길 기다린 후에 실행이 된다는 것이다.

  • useEffect는 인자로 콜백함수를 받고, 그 내부에 원하는 작업을 처리해줄 코드를 작성해주면 된다.


형태는 두가지가 존재한다.

useEffect(() => { 
  // 작업 
})

1️⃣ useEffect의 인자로 하나의 콜백함수만 받는 형태

  • 컴포넌트가 렌더링 될 때마다 매번 콜백이 실행
  • 맨 처음 화면에 렌더링 될 때, 다시 렌더링 될 때 실행
useEffect(() => { 
  // 작업 
},[value])

2️⃣ useEffect의 첫 번째 인자로 콜백 함수, 두 번째 인자로는 배열을 받는 형태

  • 배열 = Dependency array
  • [ ] : 빈 배열이면 컴포넌트가 맨 처음 화면에 렌더링 될 때 실행
  • [값] : 배열 안의 값이 바뀔 때만 실행

3️⃣ clean up

예를 들어 타이머를 실행했을 때 더 이상 타이머가 필요 없다면 타이머를 멈추는 작업해줘야 한다. 정리 작업을 처리해 주려면 useEffectreturn 값으로 함수를 넣어주면 된다. 함수 안에서 원하는 정리 작업을 수행할 수 있다.

useEffect(() => { 
  // 작업 
  return () => {
  // 작업끝 처리 claen up 
},[])

함수를 리턴하면 해당 컴포넌트가 언마운트 될 때 혹은 다음 렌더링시 불려질 useEffect가 실행되기 이전에 이 함수가 실행이 된다.


예제를 보면서 파헤쳐보자.

여기 업데이트를 누를 때마다 카운트가 하나씩 증가하는 코드가 있다.

import { useState } from "react";

export default function UseEffect() {
  const [count, setCount] = useState(1);

  const handleCountUpdate = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <button onClick={handleCountUpdate}>update</button>
      <span>count:{count}</span>
    </div>
  );
}
  • useEffect를 사용하기 위해 상단에 import를 해주고, 가장 기본적인 useEffect를 만들어준다.

  • useEffect 안에는 콜백을 하나 만들고, 이 안에 들어 있는 콜백은 컴포넌트가 렌더링 될 때마다 불려지게 된다.

import { useState, useEffect } from "react";

export default function UseEffect() {
  const [count, setCount] = useState(1);

  const handleCountUpdate = () => {
    setCount(count + 1);
  };
  
  // 렌더링 될떄마다 매번 실행 
  useEffect(() => {
   console.log('렌더링 🐰')
  })

  return (
    <div>
      <button onClick={handleCountUpdate}>update</button>
      <span>count:{count}</span>
    </div>
  );
}

맨 처음 렌더링시 컴포넌트 실행

여기서 update 버튼을 누르면 state 가 업데이트 되면서 컴포넌트는 다시 렌더링이 된다.

정리하자면 위 useEffect의 콜백은 컴포넌트가 화면에 렌더링된 직후에 불리는 것이고, state가 변경될 때마다 리렌더링이 되는 것을 볼 수 있다.


그럼, 여기서 코드를 추가하해보자.

import { useState, useEffect } from "react";

export default function UseEffect() {
  const [count, setCount] = useState(1);
  const [name, setName] = useState("");

  const handleCountUpdate = () => {
    setCount(count + 1);
  };
  
  const handleInputChange = (e) => {
    setName(e.target.value);
  };
  
  // 렌더링 될떄마다 매번 실행 
  useEffect(() => {
   console.log('렌더링 🐰')
  })

  return (
    <div>
      <button onClick={handleCountUpdate}>update</button>
      <span>count:{count}</span>
      <br></br>
      <input type="text" value={name} onChange={handleInputChange} />
      <span>name: {name}</span>
    </div>
  );
}

input에 값을 업데이트할 때마다 handleInputChange 함수가 불리기 때문에 setName이 불리면서 name 스테이트 값이 업데이트 된다. 즉 useEffect안의 콜백이 계속 호출되는 것을 볼 수 있다. 리렌더링이 계속된다 !

seEffect함수가 매번 렌더링 될때마다 실행이 되니까 만약 useEffect 함수 안에서 무거운 작업을 하면 매번 불려지는 것이니 굉장히 비효율 적이다.


그럼 여기서 name 이 실행될 때는 무시하고 count가 업데이트 될 때만 실행시키고 싶으면 ?

useEffect에 두 번째 인자로 count를 넣어준다. 이제 useEffect는 맨 처음 컴포넌트가 화면에 렌더링 되었을 때, 그리고 count 값이 변화될 때만 불리게 될 것이다.

import { useState, useEffect } from "react";

export default function UseEffect() {
  const [count, setCount] = useState(1);
  const [name, setName] = useState("");

  const handleCountUpdate = () => {
    setCount(count + 1);
  };
  
  const handleInputChange = (e) => {
    setName(e.target.value);
  };
  
  // 마운팅 + [count]가 변화될 때마다 실행됨
  useEffect(() => {
    console.log("count 변화 🐥");
  }, [count]);

  return (
    <div>
      <button onClick={handleCountUpdate}>update</button>
      <span>count:{count}</span>
      <br></br>
      <input type="text" value={name} onChange={handleInputChange} />
      <span>name: {name}</span>
    </div>
  );
}

이제 반대로 count가 업데이트될 때는 무시하고, name 값이 바뀔 때만 출력을 해보자.

import { useState, useEffect } from "react";

export default function UseEffect() {
  const [count, setCount] = useState(1);
  const [name, setName] = useState("");

  const handleCountUpdate = () => {
    setCount(count + 1);
  };
  
  const handleInputChange = (e) => {
    setName(e.target.value);
  };
  
 // 마운팅 + [name]가 변화될때마다 실행됨
    useEffect(() => {
      console.log("name 변화 🐬");
    }, [name]);

  return (
    <div>
      <button onClick={handleCountUpdate}>update</button>
      <span>count:{count}</span>
      <br></br>
      <input type="text" value={name} onChange={handleInputChange} />
      <span>name: {name}</span>
    </div>
  );
}

맨 처음 마운팅 될 때만 useEffect를 실행시켜 주고 싶다면 빈 배열을 넣어준다.

import { useState, useEffect } from "react";

export default function UseEffect() {
  const [count, setCount] = useState(1);
  const [name, setName] = useState("");

  const handleCountUpdate = () => {
    setCount(count + 1);
  };
  
  const handleInputChange = (e) => {
    setName(e.target.value);
  };
  
  // 맨 처음 렌더링이 될 때만 실행 
    useEffect(() => {
      console.log("name 변화 🐬");
    }, []);

  return (
    <div>
      <button onClick={handleCountUpdate}>update</button>
      <span>count:{count}</span>
      <br></br>
      <input type="text" value={name} onChange={handleInputChange} />
      <span>name: {name}</span>
    </div>
  );
}


useEffect에서 clean up 적용해보기

먼저 파일 두개를 만들어주자.
1️⃣ 메인 컴포넌트

import { useState } from "react";
import { Timer } from "../../../../src/components/Timer";

export default function useEffect() {
  const [showTimer, setShowTimer] = useState(false);

  return (
    <div>
    // showTimer가 true 일때만 Timer 를 보여주자. (토글)
      {showTimer && <Timer />}
      <button onClick={() => setShowTimer(!showTimer)}>Toggle Timer</button>
    </div>
  );
}

2️⃣ Timer 컴포넌트

import { useEffect } from "react";

export const Timer = (props) => {
  // 화면에 처음 렌더링시 실행, 두번째 인자로 빈 배열 넣어주기
  // 맨 처음 마운팅이 되었을때  useEffect 안에 있는 콜백함수가 실행될텐데 setInterval의 인자로 들어가 있는 콜백을 1초마다 한번씩 계속해서 반복해서 부를 거다.
  useEffect(() => {
    const timer = setInterval(() => {
      console.log("타이머 돌아가는 중...");
    }, 1000);
  }, []);

  return (
    <div>
      <span>타이머를 시작합니다. 콘솔을 보세요!</span>
    </div>
  );
};

토글타이머를 누르면 타이머 컨포넌트가 마운트가 되고, 토클 타이머를 한번 더 누르면 타이머 컴포넌트가 사라진다. unMount 이때 토글 타이머를 누르면 타이머 컴포넌트가 마운트가 되면서 (처음 렌더링) useeffect가 실행이 되고 1초에 한 번씩 콘솔을 찍는다.

여기서 타이머 컴포넌트가 사라지면서 state의 리렌더링도 끝나는지 콘솔로 확인해 보자.

계속 돌아간다. 왜냐하면 만든 타이머를 정리하지 않았기 때문이다. 즉 종료를 안 해줬다!


Q. 타이머가 언마운트 되었을때 타미머도 멈추게 하려면 ?
useEffect의 return 값으로 함수를 넣어준다. 그리고 그 함수 안에서 정리작업을 해주면 된다.

import { useEffect } from "react";

export const Timer = (props) => {
  useEffect(() => {
    const timer = setInterval(() => {
      console.log("타이머 돌아가는 중");
    }, 1000);

    // 타이머 컴포넌트가 언마운트 될 때 실행된다. 타이머 정리코드 > clean up !
    return () => {
      clearInterval(timer);
      console.log("타이머가 종료되었습니다.");
    };
  }, []);

  return (
    <div>
      <span>타이머를 시작합니다. 콘솔을 보세요!</span>
    </div>
  );
};

지금까지 useEffect에 대해서 다시 알아보았다. 이 과정에서 return 값에 함수를 넣어주면 해당 지정 함수가 종료된 다는 것을 처음 알게 되었다. 그 동안 나는 useEffect를 어떻게 쓴건지..🤔 리팩토링 과정에서 유용하게 쓰일 것 같으니 저장! 그렇다고 useEffect를 남발하면 안되고, useEffect 안에서 컴포넌트의 정보를 사용하는것을 최소화 해야한다. 왜냐하면 useEffect안에서 컴포넌트 내부에 있는 값을 많이 가져다 사용할수록 렌더링 스코프에 갇힌 변수들이 많아진다는것이고 그 말은 결국, 디버깅하기 힘든 이슈가 발생할 가능성이 높아진다는것을 의미하기 때문이다.



참고
리액트 공식 홈페이지
별코딩-리액트훅스시리즈

profile
slow but sure

0개의 댓글