useId와 useEffect(컴포넌트 생명주기)

#React

useId

useId는 고유한 ID를 만들어주는 훅이다. 가령 map으로 생성한 체크박스와 라벨을 서로 연결하고 싶은 경우, id와 htmlFor 속성을 이용하는데, 이 때 ID가 고유하지 않은 경우 라벨을 선택했을 때 예상한 것과 다른 체크박스가 선택될 수 있다. 이 때 useId를 사용하면 컴포넌트 마다 고유한 ID를 부과할 수 있다.

import { useId } from 'react'

export default function App() {
	const uuid = useId();
	return (
		<>
			<input type="checkbox" id={uuid}></input>
			<label htmlFor={uuid}>체크박스</label>
		</>
	);
}

참고로 useId가 없었던 리액트 17 이전에는 고유한 ID를 부과하기 위해 서드 파티 라이브러리로 uuid를 주로 사용했다고 한다.

useEffect

useEffect는 컴포넌트의 사이드 이펙트를 제어할 때 사용하는 훅이다. 리액트 컴포넌트에서 사이드 이펙트란 JSX를 사용해서 UI를 보여주는 것 외의 작업을 말한다. 예를들어 API를 호출한다거나, 타이머를 설정할 때 useEffect를 사용한다.

useEffect는 컴포넌트의 생명주기에 따라 크게 세 군데에서 호출된다. 각각 마운트 되는 시점, 업데이트 되는 시점, 언마운트 되는 시점이다. 여기서 마운트/언마운트는 컴포넌트가 생성/소멸되는 것을 말하며, 업데이트 되는 대상은 보통 컴포넌트의 상태이다.

useEffect는 인자로 setup 함수와 dependencies 배열을 받는다. setup은 필수 항목, dependencies는 선택 항목이다. setup 함수는 각 시점에서 실행되는 함수를 넣고, dependencies에서는 의존성 배열을 넣는다.

useEffect(setup , dependencies?)

시점 별 useEffect 사용법

마운트 시점

기본적인 setup 함수와 빈 배열의 dependencies를 사용한다.

import { useEffect } from 'react';

export default function Mount() {
	useEffect(() => {
		console.log('Mounted');
	}, []);
	return <div>Mount</div>
}

위 코드를 저장하고 실행하면 ‘Mounted’가 두 번 출력되는데, 이는 개발 모드(Strict Mode)에서는 컴포넌트 테스트를 위해 마운트->언마운트->마운트 과정을 거쳐 총 두 번의 컴포넌트 마운트가 일어나기 때문이다. 배포된 코드에서는 마운트가 정상적으로 한 번만 일어난다.

업데이트 시점

기본적인 setup 함수와 업데이트를 추적할 변수가 들어있는 배열의 dependencies를 사용한다.

import { useState, useEffect } from 'react';

export default function Update() {
	const [ count, setCount ] = useState(0);
	useEffect(()=>{
		console.log(`count: ${ count }`);
	}, [ count ]);
	return (
		<div>
			<button onClick={()=>setCount(count => count + 1)}>카운트 증가</button>
		<div/>);
}

언마운트 시점

클린업 함수를 리턴하는 setup함수와 빈 배열의 dependencies를 사용한다.

// UnMount.tsx
import { useEffect } from 'react';

export default function UnMount() {
	useEffect(()=>{
		return () => console.log('Unmounted');
	}, []);
	return <div>Unmount</div>
}

// App.tsx
import { useState } from 'react';
import UnMount from './components/UnMount'

export default function App() {
	const [ show, setShow ] = useState(true);
	return(
	<>
		{ show && <UnMount /> }
		<button onClick={()=>(setShow(show => !show))}>Toggle</button>
	</>
	);
}

useEffect 훅 사례

API 호출

import { useEffect } from "react";

export default function FetchUser() {
  useEffect(()=>{
    fetch('https://jsonplaceholder.typicode.com/users')
    .then((response) => response.json())
    .then((data)=>console.log(data));
  }, []);
  return(
    <div>FetchUser</div>
  );
}

타이머 설정

import { useEffect, useState } from "react";

export default function Timer() {
  const [seconds, setSeconds] = useState(0);
  useEffect(()=>{
    const interval = setInterval(() => {
      setSeconds((prev) => prev + 1);
    }, 1000);
    return () => clearInterval(interval);
  }, []);
  return(
    <p>timer: {seconds} seconds</p>
  );
}

실시간 이벤트 처리(스크롤)

import { useEffect } from "react";

export default function ScrollTracker() {
  useEffect(()=>{
    const handleScroll = () => {
      console.log('현재 스크롤 위치:', window.scrollY);
    };
    window.addEventListener('scroll', handleScroll);
    return () => {
      window.removeEventListener('scroll', handleScroll);
    };
  }, []);
  return(
    <div style={{ height: '200vh' }}>스크롤 해 보세요.</div>
  );
}

자동 저장 기능 구현(localStorage)

import { useEffect, useState } from "react";

export default function AutoSaveForm() {
  const [formData, setFormData] = useState('');
  useEffect(()=>{
    const savedData = localStorage.getItem('savedFormData');
    if (savedData) {
      setFormData(savedData);
    }
  }, []);
  useEffect(() => {
    const timeoutId = setTimeout(() => {
      localStorage.setItem('savedFormData', formData);
    }, 1000);
    return () => clearTimeout(timeoutId);
  }, [formData]);
  return(
    <textarea
    value={formData}
    onChange={e=>setFormData(e.target.value)}
    placeholder="입력한 내용을 자동으로 저장합니다."></textarea>
  );
}

실시간 통신 기능 구현

import { useState, useEffect } from "react";

export default function WebSocketTest() {
  const [messages, setMessages] = useState<string[]>([]);
  const [message, setMessage] = useState('');
  const [socket, setSocket] = useState<WebSocket | null>(null);

  useEffect(()=>{
    const socket = new WebSocket('wss://echo.websocket.org');
    setSocket(socket);
    socket.onmessage = (event) => {
      setMessages((prev) => [...prev, `서버: ${event.data}`]);
    };
    socket.onerror = (error) => {
      console.log('웹 소켓 오류:', error);
    };
    socket.onclose = () => {
      console.log('웹 소켓 연결 종료')
    };
    return () => {
      socket.close();
    };
  },[]);

  const handleSendMessage = () => {
    if (socket && socket.readyState === WebSocket.OPEN && message) {
      socket.send(message);
      setMessages((prev) => [...prev, `나: ${message}`]);
      setMessage('');
    } else {
      alert('서버 연결이 끊겼습니다.');
    }
  }
  
  return(
    <div>
      <div>
        {messages.map((msg, index) => (
          <div key={index} className="message">{msg}</div>
        ))}
      </div>
      <div>
        <input value={message} 
          onChange={e=>setMessage(e.target.value)}
          placeholder="메세지를 입력하세요." /> 
        <button onClick={handleSendMessage}>전송</button>
      </div>
    </div>
  );
}