컴포넌트 안에서 데이터를 저장하는 가장 단순한 방법은 let 또는 const로 변수를 선언하는 것이다.
하지만 이렇게 선언한 변수는 변경되더라도 React가 그 변화를 감지할 수 없기 때문에 화면이 다시 렌더링되지 않는다.
즉, 값은 바뀌지만 UI는 업데이트되지 않는다.
그래서 렌더링과 관련된 데이터는 일반 변수로 두지 않고 “상태(State)”로 관리해야 한다.
State란 컴포넌트 내부에서 보관하고 변경할 수 있는 데이터이며, 사용자 입력·이벤트·API 응답 등으로 변화할 수 있다.
React는 state 값이 바뀌면 자동으로 해당 컴포넌트를 다시 렌더링해서 화면에 그 변화를 반영한다.
리액트에서는 원래 상태(state) 관리나 라이프사이클 작업을 하려면 클래스형 컴포넌트를 써야 했다. 하지만 함수형 컴포넌트는 그냥 함수이기 때문에 이런 기능을 바로 쓸 수 없었다. 그래서 React는 Hook을 도입했다.
Hook이 생기면서 이제는 굳이 클래스를 만들지 않아도, 일반 함수 형태의 컴포넌트 안에서도 상태를 유지하고 업데이트할 수 있고, 렌더링 이후 작업 같은 라이프사이클적인 행동도 처리할 수 있게 되었다.
함수형 컴포넌트는 함수이기 때문에, 함수 안의 지역 변수는 렌더링이 다시 일어나면 매번 초기화된다. 그래서 컴포넌트 안에서 어떤 값을 계속 유지하고 변경하면서 화면에도 반영하고 싶을 때 useState를 쓴다. 그리고 이렇게 상태가 바뀌고 화면이 렌더링될 때마다 어떤 “추가 작업”을 하고 싶을 때 그 흐름을 따라다니며 실행되는 게 useEffect이다.
또 어떤 값이 전역 형태처럼 여러 자식 컴포넌트에서 공유되어야 한다면, props를 계속 뚫고 내려보내는 방식 대신 useContext로 필요한 컴포넌트에서 바로 꺼내써서 더 편하게 관리할 수 있다.
상태가 조금 더 복잡하고 조건에 따라 분기하면서 정확하게 상태를 업데이트해야 하는 구조라면, useState보다 명확한 방식으로 업데이트 로직을 관리할 수 있는 useReducer가 적합하다.
그리고 필요 없는 재계산이나 렌더링을 줄이고 싶을 때 성능 최적화를 위해 계산 결과를 기억해두는 useMemo, 특정 함수를 그대로 재사용하고 싶을 때 useCallback을 사용해서 불필요한 연산이나 재정의를 줄인다.
| Hook | 설명 |
|---|---|
useState | 컴포넌트의 상태(state)를 관리 |
useEffect | 컴포넌트의 라이프사이클 효과(side effects) 처리 |
useContext | 전역 상태를 쉽게 사용 (Context API) |
useRef | DOM 요소 또는 값을 저장 (값이 변경되어도 리렌더링되지 않음) |
useReducer | useState보다 복잡한 상태 관리를 처리 |
useMemo | 연산량이 많은 함수의 결과를 저장하여 성능 최적화 |
useCallback | 함수를 메모이제이션하여 불필요한 렌더링 방지 |
useLayoutEffect | useEffect과 비슷하지만 렌더링 이후 바로 실행 |
useImperativeHandle | ref를 통해 부모가 자식 컴포넌트의 특정 기능을 호출할 수 있도록 설정 |
현업에서 React를 사용하다 보면 가장 자주 마주치는 Hook이 useState이다.
useState는 컴포넌트가 가진 상태(state)를 관리하고, 그 상태가 변경될 때마다 화면을 자동으로 갱신하도록 도와주는 핵심 도구이다.
state는 컴포넌트가 가질 수 있는 “현재 상태”를 의미한다.
useState는 이 상태를 간편하게 생성하고 업데이트할 수 있게 하는 React Hook이다.
const [state, setState] = useState(초기값);
여기서 state는 현재 상태를 담고, setState는 그 상태를 변경하는 함수이다.
setState를 호출하면 해당 컴포넌트는 다시 렌더링되어 새로운 상태가 화면에 반영된다.
import { useState } from 'react';
function App() {
const [time, setTime] = useState(1);
const handleClick = () => {
let newTime;
if (time >= 12) {
newTime = 1;
} else {
newTime = time + 1;
}
setTime(newTime);
console.log('업데이트!!!');
};
return (
<div>
<span>현재 시간: {time}시</span>
<button onClick={handleClick}>Update</button>
</div>
);
}
export default App;
이 예제에서 time은 컴포넌트의 상태이며, setTime으로 상태를 변경할 수 있다.
버튼을 클릭할 때마다 time 값이 1씩 증가하며, 12를 넘으면 다시 1로 돌아간다.
상태가 바뀌면 컴포넌트는 자동으로 다시 렌더링된다.
컴포넌트가 렌더링될 때마다 오래 걸리는 작업을 반복 실행하면 성능 저하가 발생할 수 있다.
이때 useState는 콜백 형태의 초기화 함수를 제공하여 최초 렌더링 시 한 번만 실행되도록 만들 수 있다.
콜백 함수란 다른 함수의 인자로 전달된 함수이다.
import { useState } from 'react';
const heavyWork = () => {
console.log("엄청 무겁고 오래걸리는 작업!!");
return ['홍길동', '이순신'];
}
function App() {
// const [names, setNames] = useState(heavyWork()); // 매 렌더링마다 실행됨
const [names, setNames] = useState(() => {
return heavyWork(); // 최초 렌더링 시에만 실행됨
});
const [input, setInput] = useState('');
const handleInputChange = (e) => {
setInput(e.target.value);
};
const handleUpload = () => {
setNames((prevState) => {
console.log('이전 state :', prevState);
return [input, ...prevState];
});
setInput(''); // 입력창 초기화
};
return (
<div>
<input
type="text"
value={input}
onChange={handleInputChange}
/>
<button onClick={handleUpload}>Upload</button>
{names.map((name, idx) => (
<p key={idx}>{name}</p>
))}
</div>
);
}
export default App;
React에서 배열의 데이터를 화면에 렌더링할 때는 map() 함수를 자주 사용한다.
이때 각 요소에는 반드시 고유한 key 값을 부여해야 한다.
보통 간단한 예제에서는 배열의 인덱스를 key로 사용한다.
useState로 상태를 변경할 때,
새로운 상태가 이전 상태에 의존하는 경우가 자주 있다.
이럴 때는 setState의 인자로 콜백 함수를 전달하는 것이 안전하고 권장되는 방법이다.
setNames((prevState) => {
return [input, ...prevState];
});
이 코드는 이전 상태(prevState)를 기반으로 새로운 배열을 만들어 반환한다.
이렇게 하면 비동기 렌더링 환경에서도 안전하게 상태를 업데이트할 수 있다.
useEffect는 컴포넌트가 화면에 렌더링된 이후에 실행되는 작업(부수효과, side effect)을 처리하기 위한 Hook이다. 비동기처리에 자주 쓰인다.
React의 렌더링은 “화면에 그리는 일”까지만 담당하고,
데이터 요청, 타이머 설정, 이벤트 등록 같은 부수적인 로직은 useEffect 안에서 처리한다.
useEffect(() => { 작업 });→ 컴포넌트가 렌더링될 때마다 실행된다.
useEffect(() => { 작업 }, [value]);→ 첫 렌더링과value값이 바뀔 때 실행된다.
여기서 [value]는 의존성 배열(dependency array)이다.
React는 이 배열을 관찰하면서, 내부 값이 변경될 때만 useEffect를 다시 실행한다.
즉, 불필요한 재실행을 방지하고 성능을 최적화할 수 있다.
의존성 배열을 비워 두면([]),
컴포넌트가 처음 렌더링될 때 한 번만 실행된다.
보통 API 요청이나 초기 데이터 로드 시에 많이 사용된다.
useEffect(() => {
fetch('https://api.example.com/users')
.then(res => res.json())
.then(data => setUsers(data));
}, []);
React가 effect를 정리(clean-up)하는 시점은 정확히 언제일까?
React는 컴포넌트가 마운트 해제되는 때에 정리(clean-up)를 실행합니다.
useEffect 안에서 return 문을 사용하면,
컴포넌트가 사라지거나(unmount) 다음 렌더링이 일어나기 전에 정리 작업을 수행한다.
useEffect(() => {
console.log('타이머 시작!');
const timer = setInterval(() => {
console.log('1초마다 실행');
}, 1000);
// cleanup 함수
return () => {
clearInterval(timer);
// 컴포넌트가 없어질 때 자동으로 clearInterval을 호출해 메모리 누수를 막을 수 있다.
console.log('타이머 정리 완료');
};
}, []);
useEffect는 다음과 같은 상황에서 주로 사용된다.
setInterval, setTimeout)를 설정할 때scroll, resize 등)을 등록할 때관심사별로 effect를 나눠라
한 effect 안에 여러 역할을 동시에 넣지 말고, “무엇을 하는가” 기준으로 분리한다.
예를 들면 문서 title 변경용 effect 하나, API 구독용 effect 하나처럼 따로 관리한다.
이렇게 하면 각각의 로직이 독립되고 수정 시 영향을 추적하기 쉬워진다.
의존성 배열로 실행 시점을 명확히 제어하라
effect는 의존성 배열 값의 변화를 기준으로 실행 여부가 결정된다.
[count]라면 count가 바뀔 때만 실행하고, []라면 mount/unmount 시 딱 한 번만 실행된다.
즉, “언제 실행해야 하는가”를 의존성 배열에 명시하는 것이, 불필요한 재실행을 막고 사이드 이펙트를 예측 가능하게 만든다.
useRef는 렌더링과 상관없이 값을 저장할 수 있는 저장 공간이며,
DOM 요소에 직접 접근하는 용도로도 자주 사용되는 Hook이다.
const ref = useRef(initialValue);
ref.current라는 프로퍼티에 값이 저장되며,
이 값은 컴포넌트가 다시 렌더링되어도 유지된다.
state와는 다르게 ref.current를 변경해도 컴포넌트는 재렌더링되지 않는다.
그래서 “렌더링을 발생시키지 않아야 하는 값”을 다룰 때 유용하다.
state는 바뀌면 화면을 다시 그리지만(ref는 그렇지 않음)
UI에 반영할 필요 없는 값이라면 ref가 더 효율적이다.
import React, { useState, useRef } from 'react';
function App() {
const [count, setCount] = useState(0);
const countRef = useRef(0);
console.log('렌더링...');
const increaseCountState = () => {
setCount(count + 1);
};
const increaseCountRef = () => {
countRef.current = countRef.current + 1;
console.log('Ref:', countRef.current);
};
return (
<div>
<p>State: {count}</p>
<p>Ref: {countRef.current}</p>
<button onClick={increaseCountState}>State 올려</button>
<button onClick={increaseCountRef}>Ref 올려</button>
</div>
);
}
export default App;
state 버튼을 누르면 화면의 숫자가 변하지만,
ref 버튼은 숫자는 내부적으로 올라가지만 화면은 그대로 — 이 차이가 핵심이다.
ref는 DOM 요소를 직접 가리킬 수도 있다.
이렇게 하면 Javascript의 querySelector 없이 input에 바로 포커스를 줄 수 있다.
import React, { useEffect, useRef } from 'react';
function App() {
const inputRef = useRef();
useEffect(() => {
inputRef.current.focus(); // 컴포넌트 첫 렌더링 시 자동 focus
}, []);
const login = () => {
alert(`환영합니다 ${inputRef.current.value}!`);
};
return (
<div>
<input ref={inputRef} type="text" placeholder="username" />
<button onClick={login}>로그인</button>
</div>
);
}
export default App;