UMC 8th Web 워크북

🫂왜 useEffect를 쓰는가 (Umc 워크북 3주차 강의.)

minnote29 2025. 4. 4. 16:45

📌 useEffect는 리액트에서 함수형 컴포넌트, sideEffect를 잘 처리하기 위해 제공되는 훅이다.

 

 

useEffect – React

The library for web and native user interfaces

ko.react.dev

 

  • 생명주기 메소드를 하나로 통합해서 수행할 수 있게 해준다라고 생각하면 되겠다.
  • useEffect는 보통 데이터를 가져올 때 많이 활용하고,이벤트 리스너를 윈도우나 document에 스크롤, 마우스, 키보드 등과 관련된 걸 등록을 했으면, 그것들을 remove 리스너를 통해 언마운트 해주는 역할도 한다고 볼 수 있다.
  • 그리고 웹 소켓이나 스트리밍, 푸시 같은 것을 할 때, 데이터를 구독하고, 조작을 직접 할 때도 많이 사용한다.
  • 컴포넌트가 렌더링되는 것과 별개로, 다른 연산을 하고 렌더 트리에 영향을 미치는 작업을 하는 것을 '부수 효과'라고 부른다.

 

  • 이번 강의에서는 02-useEffect 폴더를 생성해서 작업을 하는 것 같다.
  • 아래는 예제를 위해 바꾼 코드이다.

 

 

📌App.tsx

import UseEffectPage from './02-useEffect/UseEffectPage';
import './App.css';

export default function App() {
  return (
    <>
      <UseEffectPage />
    </>
  )
}

 

  • 별 차이는 없겠지만, 좀더 원활한 이해를 위해서 강의와 똑같이 pnmp을 사용했다.
  • 이번에도 tailwind css 사용을 위해, index.css에 @import 'tailwindcss'를 적어주고, vite.config.ts에 plugins: [react(), tailwindcss()], 를 적어주면 된다. 

 

 

📌아래는 예제이다.

import { useState } from "react"

export default function UseEffectPage() {
  const [count, setCount] = useState(0);

  const handleIncrease = () => {
    setCount((prev) => prev +1);

    console.log(count);
  };
  
  return (
    <div>
      <h3>
        UseEffectPage
      </h3>
      <h1>{count}</h1>
      <button onClick={handleIncrease}>증가</button>
    </div>
  )
}
  • 이 예제에서는 증가를 비동기적으로 처리되게 하는 걸 알 수 있다.
  • handleIncrease 작업이 화면에 업데이트 되기 전의 값이 기억이 되는 형태이다.
  • 근데, 화면이 업데이트되고 난 후에 값을 받아보고 싶다면, 아래 코드처럼 바꾸면 된다.

 

  • 자세히 설명하면 일단, useEffect는 다음의 형태를 취한다.
useEffect(() => {}, []);
  • {} 이 함수 내부에 실행하고 싶은 코드를 넣으면 된다.
  • [] 이것은 dependency array(의존성 배열)이라는 것인데 이는 useEffect에서 매우 중요한 부분으로, 빈 값일 경우는 새로고침을 눌렀을 때 (화면이 새로 켜졌을 때-mount 됐을 때) 한번 부수효과가 일어난다. 
[-> useState를 통해 값을 불러옴 -> useEffect가 있다는 것을 리액트에 알림 -> JSX 컴포넌트가 반환이 됨 -> count도 당연히 찍힘 -> 그 다음, console.log가 실행이 됨.] 이 순서가 중요하다.
  • useEffect 안에 console.log(count);를 통해 실행을 해보면, 증가 눌러도 아무 동작을 안 할 것이다.
  • 여기서 알아야 될 사실이 있다면, 리렌더링 조건이다.
  • 리렌더링 조건은 상태가 변화해야 한다. 근데 useEffect도 여러번 실행을 시키고 싶으면 의존성 배열에 조건문을 추가해야 한다.
  • 즉, count를 업데이트할 때마다 useEffect 효과를 계속 실행시켜주고 싶다 하면 의존성 배열에 count를 넣어주면 된다. 이러면 이제 증가를 눌러도 바로바로 업데이트 되는 걸 알 수 있다.  

=> 결과적으로, handleIncrease에 넣은 것과 다르게, useEffect를 활용한 곳에서는 이전 값이 찍히는 것이 아니라 화면이 업데이트 된 이후의 setState로 업데이트한 값을 반환한 이후에 생성하기 때문에 화면이 항상 최신 값으로 찍히게 된다. 이 동작 과정을 이해하는 것이 상당히 중요하다. 아래가 지금까지 실습한 코드이다. 

import { useEffect, useState } from "react"

export default function UseEffectPage() {
  const [count, setCount] = useState(0);

  const handleIncrease = () => {
    setCount((prev) => prev +1);
    console.log('setState', count);
  };
  
  useEffect(() => {
    //실행하고 싶은 코드
    console.log(count);

    // 의존성 배열이 빈 배열일 경우

    // 의존성 배열 (dependency array)
  }, [count]); // []: 빈 배열

  return (
    <div>
      <h3>
        UseEffectPage
      </h3>
      <h1>{count}</h1>
      <button onClick={handleIncrease}>증가</button>
    </div>
  )
}

 

 

 

📌 또한, useEffect를 활용할 때 return 함수를 적게 될 텐데((optional) return funciton을 적는데), 이 useEffect 같은 경우는 여러번 동작할 수도 있다.

  • 일단, 자세히 알아보기 위해 return function이랑 비슷한 역할을 하는 cleanup function을 이용해 볼 것이다. 이는 청소하는 함수라고 생각하면 되는데,
return () => {
      console.log('청소하는 함수입니다.')
    }
  • 이걸 실행하면 useEffect에서 종속성 값이 변경이 될 때마다 useEffect 훅의 동작 방식은 destroy가 된다. 그때, clenup function을 통해 종료를 하게 되는데 만약 위의 cleanup 함수 없이 종료를 시킬 경우는 상태가 변경됐을 때 useEffect 코드전체가 destroy 됐다가 다시 만들어지기 때문에 계속 값이 겹치면서 실행이 될 것이다.

 

 

📌 더욱 자세한 내용은 UseEffectCounterPage.tsx에서 설명한다. 

  • 여기서는 컴포넌트 2개를 이용할 건데 분리하면 헷갈리기 때문에 하나에 적는다. (export는 2개 있으면 안 되서 하나에만.)
import { useEffect, useState } from "react"

export default function Parent() {
  const [visible, seVisible] = useState(false);

  return (
    <>
        <h1>같이 배우는 리액트 #2 useEffect</h1>
        <button onClick={() => seVisible(!visible)}>
            {visible ? '숨기기' : '보이기'}
        </button>
        {visible && <Child/>}
    </>
  )
}

function Child() {
    useEffect(() => {
        let i = 0;
        const countInterval = setInterval(() => {
            console.log('Number => ' + i);
            i++;
        }, 1_000); // 1초마다 타이머가 동작하는 걸 볼 수 있다.
    }, []);

    // 마진 20 텍스트 4xl
    return <div className='mt-20 text-4xl'>Child</div>
}
  • 이렇게 되는데 만약에 숨겼을 때, 타이머가 안 동작하게 하고 싶다면, 메모리 상에 남아서 계속 실행되고 있을 것이다.
  •  근데 여기서 mount를 한번 더 시킨다면? 한번 더 동작을 할 것이다. 여기서 이제 문제가 발생되는 걸 알 수 있다. 보이기를 하면 타이머 2개가 작동해서 흐름이 꼬이는 현상을 볼 수 있을 것이다.
  • 이 현상이 일어나는 이유가 cleanup function을 사용하지 않아서 생기는 문제라는 걸 알 수 있다.
  • 이걸 해결하기 위해서 위의 코드를 바꿔준다. 
import { useEffect, useState } from "react"

export default function Parent() {
  const [visible, seVisible] = useState(false);

  return (
    <>
        <h1>같이 배우는 리액트 #2 useEffect</h1>
        <button onClick={() => seVisible(!visible)}>
            {visible ? '숨기기' : '보이기'}
        </button>
        {visible && <Child/>}
    </>
  )
}

function Child() {
    useEffect(() => {
        let i = 0;
        const countInterval = setInterval(() => {
            console.log('Number => ' + i);
            i++;
        }, 1_000); // 1초마다 타이머가 동작하는 걸 볼 수 있다.
        
        return () => {
            console.log('언마운트 될 때 실행됩니다.');
            clearInterval(countInterval);
        }
    }, []);


    // 마진 20 텍스트 4xl
    return <div className='mt-20 text-4xl'>Child</div>
}
  • 이렇게 하면 useEffect 함수가 destroy되기 전에 cleanup function이 작동해서 작동을 하는 걸 알 수 있고, cleanup function에 clearInterval(counterInterval);을 해주면 숨기기를 했을 때 타이머가 동작하지 않도록 할 수 있다. 거기에, 다시 보이기를 누르면 다시 1개의 타이머만 동작하는 걸 볼 수 있다. 

 

📌 다음은 useEffectError.tsx이다.

  • useEffect에서는 절대 하면 안 되는 원칙이 하나 있다. 
import { useEffect, useState } from "react";

export default function UseEffectError() {
  const [counter, setCounter] = useState(0);

  const handleIncrease = () => {
    setCounter((counter) => counter + 1);
  };

  useEffect(() => {
    setCounter((counter) => counter + 1);
  });
 
  return (
    <div onClick={handleIncrease}>{counter}</div>
  )
}
  • 이 코드의 useEffect 함수를 보면 저런 식으로 상태를 업데이트시켜주는 것을 직접 넣으면 안 된다.
  • 이런 식으로 할 경우, 값이 무한으로 증가가 된다. (무한 렌더링 발생.)

  • 일단, 의존성 배열을 빈 배열로 써주면, 화면이 업데이트 될 때 딱 한번만 즉, setCounter로 counter를 한번 증가시켜주겠다는 것이다.  
  • 여기에 [counter]를 넣어주게 되면, 아래같은 경우가 발생할 것이다. 
import { useEffect, useState } from "react";

export default function UseEffectError() {
  const [counter, setCounter] = useState(0);

  const handleIncrease = () => {
    setCounter((counter) => counter + 1);
  };

  useEffect(() => {
    // 1. 초기 렌더링 시작(counter ++)
    setCounter((counter) => counter + 1);

    // 2. counter 값이 변경될 때마다 실행
  }, [counter]);
  // 1번과 2번 과정이 반복해서 일어나니까, 무한 렌더링 사태가 일어남.
 
  return (
    <div onClick={handleIncrease}>{counter}</div>
  )
}
  • 만약에, 데이터가 요청하는 코드가 들어갔다고 생각해보면, 요청이 무한히 가고 DB에서는 우리의 접근을 막게 될 것이다. (이에 관한 처리가 있으면..)
  • 그러므로, 이런 패턴은 절대 쓰면 안 되는 패턴이다. 
  • 즉, useEffect에서 상태를 업데이트하는 함수와 그 함수에 대한 변수를 같이 넣을 경우, 무한 렌더링이 발생하며 에러가 발생하게 되므로 이런 식으로 절대 쓰면 안 된다.