useId와 useEffect(컴포넌트 생명주기)
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>
);
}