본문 바로가기

043. React - useEffect

codeConnection 2024. 5. 23.

useEffect란?

리액트에서 컴포넌트의 사이드 이펙트를 제어하는 훅.

사이드 이펙트는 직역하면 '부작용'이고, 리액트에서 표현하는 사이드 이펙트는 어떤 기능에서 '의도하지 않은 부수적인 효과', 내지 '파생되는 효과' 정도의 개념이다.

사용방법

  1. import하기
// state import 하기
import { useEffect } from "react";
  1. 원형 작성하기
const [count, setCount] = useState(0);
useEffect(()=>{},[]);
// 두 개의 인자를 받는데 첫번째는 콜백함수, 두번째는 의존성 배열(Dependency Array)
// 해석은 Deps의 값이 변경되면 콜백함수가 실행됨.

위와 같이 useState 훅과 동시에 사용 가능. 카운트 앱에서 사용되는 count state인데, setCount로 값이 변경될 때마다 useEffect 훅의 콜백함수를 실행시킬 수 있다.

  1. 사용하기
const [count, setCount] = useState(0);
useEffect(()=>{
    console.log(`카운트가 ${count}으로 변경되었습니다.`);
},[count]);
// 이렇게 작성하면 count state가 변경될 때마다 콘솔에 출력됨.
  1. Deps에 여러 개 값 추가하기

Deps는 배열이기 때문에 여러 개의 값의 변화를 감지할 수 있다.

const [count, setCount] = useState(0);
const [input, setInput] = useState('');

useEffect(()=>{
    console.log(`카운트가 ${count}으로 변경되었습니다. 현재 입력필드의 값은 ${input}입니다.`);
},[count, input]);

return (
    <>
        <input value={input} onChange={(e)=>setInput(e.target.value)}/>
    </>
)

useState 훅을 바로 출력하지 않고 useEffect 훅을 사용해야 하는 이유

const [count, setCount] = useState(0);
const [input, setInput] = useState('');

return (
    <>
        <input value={input} onChange={(e)=>
        setInput(e.target.value);
        console.log(count);}/>
    </>
)

예를 들어서 위처럼 useEffect 훅을 이용하지 않고 count state의 변화를 직접 console에서 찍어볼 수 없는지 의문이 든다.

그런데 실제로 저 코드를 콘솔에 출력해보면 카운트가 증가되는 것이 한 박자 늦게 뜬다. 그러니까 나는 카운트를 1 증가 시켰는데 0이 출력되고, 2가 되어야 하는데 1이 출력된다는 것이다.
그 이유는 useState훅은 비동기적으로 실행되기 때문이다. 즉 코드의 흐름대로 작성 순서대로 실행되는 것이 아니라 1. 화면에 렌더링 되는 부분부터 렌더링(return문 내부) -> 2. 자바스크립트 함수부터 실행(console.log) -> 3. 가장 마지막에 useState훅 실행 순서대로인 것이다. useState 훅은 코드 흐름대로 Mount만 되지 실행은 가장 마지막에 한꺼번에 모아서 처리하기 때문에 이미 콘솔에 찍히고 나서 카운트가 증가하는 현상, 즉 한 박자 늦게 나오는 현상이 발생한다.

따라서 같은 시점에서 같이 비동기적으로 실행되는 useEffect훅을 사용하면 useState의 사이드 이펙트, 즉 상태 변화를 즉각 감지할 수 있다.

useEffect로 라이프사이클 제어하기

Mount (탄생) 시에 useEffect 호출하기

사용 방법

deps에 빈 배열을 주면 됨.

useEffect(()=>{
    console.log('마운트 되었습니다');
},[]);

이렇게 하면 마운트 시에만 한 번 실행하고, 그 뒤로 컴포넌트가 업데이트 되더라도 useEffect 훅은 실행되지 않음.

웹 사이트를 개발할 때 이런 방식은 다양한 상황에서 쓰일 수 있는데, 대표적으로는 API 호출이 있음. 컴포넌트가 재 렌더링 될 때마다 API를 새롭게 호출하면 서버에 부하가 걸릴 수 있기 때문에 이럴 때 useEffect훅을 이용하면 컴포넌트를 처음 마운트 했을 때만 API 호출을 일으킬 수 있음.

활용 예시

// useEffect 사용 예시 : API 호출하기
import { useState, useEffect } from 'react';
import axios from 'axios';

function DataFetchingComponent() {
  const [data, setData] = useState(null);

  useEffect(() => {
    axios.get('/api/data')
      .then(response => {
        setData(response.data);
      })
      .catch(error => {
        console.error('Error fetching data:', error);
      });
  }, []);

  return (
    <div>
      {data ? <pre>{JSON.stringify(data, null, 2)}</pre> : 'Loading...'}
    </div>
  );
}

export default DataFetchingComponent;

// useEffect 사용 예시 : 타이머를 마운트 시 작동시키고 언 마운트 시 해제 시키기
import { useEffect } from 'react';

function TimerComponent() {
  useEffect(() => {
    const timer = setInterval(() => {
      console.log('Timer tick');
    }, 1000);

    return () => clearInterval(timer);
  }, []);

  return <div>콘솔에서 타이머를 확인하세요.</div>;
}

export default TimerComponent;


// useEffect 사용 예시 : 웹소켓 연결이나 외부 데이터 소스에 대한 서브스크립션을 설정하고, 컴포넌트가 언마운트될 때 이를 해제하는 경우

import { useEffect } from 'react';

function WebSocketComponent() {
  useEffect(() => {
    const socket = new WebSocket('ws://example.com/socket');

    socket.onmessage = (event) => {
      console.log('응답 메시지:', event.data);
    };

    return () => {
      socket.close();
    };
  }, []);

  return <div>웹소켓 연결이 활성화되었습니다. 콘솔에서 확인해보세요.</div>;
}

export default WebSocketComponent;

Update (업데이트) 시에 useEffect 호출하기

두 가지 상황이 있다.

  1. 마운트 시에 한 번 실행은 하고, 업데이트 마다 실행하는 방법
  2. 마운트 시에 실행하지 않고, 업데이트 때만 실행하는 방법

일단 두 상황 다 useEffect의 두번째 매개변수인 deps를 빈 배열도 아닌 아예 작성하지 않으면 된다.

// 마운트 시에 한 번 실행 + 업데이트 때마다 실행
useEffect(() => {
    console.log('업데이트 되었습니다.');
});

위 상황은 카운트가 계속 증가한다든지, 사용자가 Input에 타이핑을 한다든지 하는, 재 렌더링이 계속 되는 상황에서 상태가 바뀔 때마다 계속 실행된다. 물론 최초 마운트 시 1회는 같이 실행된다.

// 마운트 시에는 실행하지 않고 업데이트 할때만 실행

// useRef 훅 Import
import { useEffect, useRef } from "react";

// useRef로 컴포넌트 마운트 상태 관리
const isMount = useRef(false);

// 조건식으로 마운트 됐을 때만 함수 코드블럭을 실행하도록 설정
useEffect(() => {
    if(!isMount.current) {
        isMount.current = true;
        return;
    }
    console.log('업데이트 되었습니다.');
});

// isMount의 값이 false이면 isMount의 값을 true로 바꾸고
// 함수를 더 진행시키기 않고 return 해버리기
// 다음부터는 컴포넌트가 재 렌더링 될 때는 isMount가 이미 마운트 될 때
// true로 바뀌었기 때문에 조건문을 통과하고 코드가 업데이트 때마다 실행된다.

useRef란?

useRef 또한 React의 hook 중 하나로 current라는 프로퍼티를 가진 객체를 반환하는 훅이다. 컴포넌트의 라이프사이클 동안만 값이 유지된다는 특징이 있고, 실제 웹 개발에서는 1. DOM의 특정 요소에 접근하거나 2. 값의 변경을 추적할 때 사용한다.

1. DOM의 특정 요소에 접근
// hook import 하기
import { useEffect, useRef } from "react";

function TextInput() {
    // 초기 값 null로 할당
    const inputRef = useRef(null);

    useEffect(()=>{
         // TextInput 컴포넌트가 마운트 될 때 inputRef에 포커싱 시키기
        inputRef.current.focus();
    },[]);

    return (
        <>
        {/* HTML에서 ID 부여하듯 ref 속성으로 useRef훅을 할당한 변수를 바인딩 */}
        <input ref={inputRef} type="text"/>
        </>
    )
}

export default TextInput;
2. 값의 변경을 추적하기

useRef는 상태가 변경되어 컴포넌트가 재 렌더링 되더라도 재 렌더링 되지 않고 값이 유지되기 때문에 카운트 기능 같은 것에 사용하면 유용하다.

import { useRef, useState, useEffect } from 'react';

function Timer() {
    // 초기값 null로 설정하여 변수에 할당
  const timerRef = useRef(null);
  const [count, setCount] = useState(0);

// 마운트 되고, 업데이트 될 때마다 실행할 useEffect 훅 정의 (deps 빈 배열)
  useEffect(() => {
    timerRef.current = setInterval(() => {
      setCount(prevCount => prevCount + 1);
    }, 1000);

    return () => clearInterval(timerRef.current);
  }, []);

  return <div>Timer: {count}</div>;
}

export default Timer;

Unmount (죽음) 시에 useEffect 사용하기

마운트에 실행하는 것처럼 빈 배열을 두번째 매개변수로 전달해주고, useEffect 훅의 콜백함수로 Clean Up (정리) 함수를 사용하면 됨.

import { useEffect } from "react";

function Even() {

useEffect(()=>{

  return () => {
    // return문 안에 작성하는 것이 클린업 함수임.
  }
},[])

  return (
    <>
    <div> 짝수입니다. </div>
    </>
  )
}

이것은 언제 사용하냐면, 조건부 렌더링을 할 때 사용함. 예를 들어서 위는 짝수일 때만 카운트가 보여주는 컴포넌트인데, 카운트가 홀수일 땐 사라져야 하기 때문임.

실제 웹 개발 시에는 서버로부터 데이터를 API 호출로 받아오는 동안은 스피너 같은 로딩 컴포넌트를 띄워주고, fetch가 완료되면 Unmount 시키는 조건부 렌더링을 할 수도 있음.

import React, { useState, useEffect } from 'react';

function DataFetchingComponent() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch('/api/data')
      .then(response => response.json())
      .then(data => {
        setData(data);
        setLoading(false);
      });
  }, []);

  if (loading) {
    return <div>Loading...</div>;
  }

  return (
    <div>
      <h1>Data:</h1>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
}

export default DataFetchingComponent;

댓글