
상태 관리 : 앱의 복잡성을 줄이고, 사용자 인터페이스의 일관성을 유지하는데 중요한 역할.
state : “시간이 지나면서 바뀌는 데이터”. 버튼 클릭 횟수, 입력 폼 값 등등..
useState - 상태값과 해당 값을 업데이트하는 함수를 반환한다. 상태가 단순할 때 유용, 기본적인 상태 관리 방식
useReducer - 상태 업데이트 로직을 컴포넌트 외부로 추출하여 더 구조화된 형태로 관리할 수 있다. 복잡할 때 유용하다.
const [state, dispatch] = useReducer(reducer, initialState);
- state: 현재 상태 값
- dispatch(action): 상태를 바꾸기 위해 “액션”을 보낸다.
- reducer(state, action): 액션에 따라 새로운 상태를 반환하는 함수
유용한 상황
- 상태가 여러 필드를 가진 객체, 배열 등일 때
- 상태를 바꾸는 액션 종류가 많을 때 (ADD / REMOVE / UPDATE / TOGGLE …)
- “상태 변경 로직”을 한 곳에 모아두고 싶을 때 → 유지보수, 테스트, 리팩터링에 유리
useState 버전
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const [isSaving, setIsSaving] = useState(false);
const [error, setError] = useState(null);
async function handleSave() {
setIsSaving(true);
setError(null);
try {
await savePost({ title, content });
setIsSaving(false);
} catch (e) {
setError(e.message);
setIsSaving(false);
}
}
useReducer 버전
const initialState = {
title: '',
content: '',
isSaving: false,
error: null,
};
function reducer(state, action) {
switch (action.type) {
case 'CHANGE_FIELD':
return { ...state, [action.name]: action.value };
case 'SAVE_START':
return { ...state, isSaving: true, error: null };
case 'SAVE_SUCCESS':
return { ...state, isSaving: false };
case 'SAVE_ERROR':
return { ...state, isSaving: false, error: action.error };
default:
return state;
}
}
const [state, dispatch] = useReducer(reducer, initialState);
async function handleSave() {
dispatch({ type: 'SAVE_START' });
try {
await savePost({ title: state.title, content: state.content });
dispatch({ type: 'SAVE_SUCCESS' });
} catch (e) {
dispatch({ type: 'SAVE_ERROR', error: e.message });
}
}
1. 필요한 최소한의 상태만 둔다.
계산으로 얻을 수 있는 값은 state에 따로 저장하지 않는다. (리스트 길이, 필터링된 결과 등)
2. 중복 상태를 피한다.
싱크가 안 맞으면 버그가 될 수 있으므로, 같은 정보를 여러 곳에 state로 저장하지 않는다.
3. 항상 같이 바뀌는 값은 하나의 state로 합친다.
예를 들어, firstName과 lastName을 항상 같이 다룬다면 user객체로 묶는다.
4. 상태 올리기
둘 이상의 컴포넌트가 같은 state를 필요로 한다면 —> 그 둘의 공통 부모로 state를 올리고, props로 내려준다.
5. 상태는 불변성을 유지하기
배열은 push 대신 concat, filter, map을 사용하고,
객체는 직접 수정이 아닌, { ...obj, something: newValue } 같이 복사해서 새 객체로 반환한다.
여러 컴포넌트에서 활용하고 싶을 때
“useReducer로 상태·로직 관리 + Context”로 전역에서 활용할 수 있다.
import { createContext, useContext, useReducer } from "react";
const CounterStateContext = createContext(null);
const CounterDispatchContext = createContext(null);
function counterReducer(state, action) {
switch (action.type) {
case "INC":
return state + 1;
case "DEC":
return state - 1;
default:
return state;
}
}
export function CounterProvider({ children }) {
const [count, dispatch] = useReducer(counterReducer, 0);
return (
<CounterStateContext.Provider value={count}>
<CounterDispatchContext.Provider value={dispatch}>
{children}
</CounterDispatchContext.Provider>
</CounterStateContext.Provider>
);
}
export function useCounterState() {
return useContext(CounterStateContext);
}
export function useCounterDispatch() {
return useContext(CounterDispatchContext);
}
이것을
import { useCounterState, useCounterDispatch } from "./CounterContext";
function SomeComponent() {
const count = useCounterState();
const dispatch = useCounterDispatch();
return (
<div>
<p>{count}</p>
<button onClick={() => dispatch({ type: "INC" })}>+1</button>
</div>
);
}
이렇게 하면 redux 느낌의 구조를 리액트 내장 기능만으로 만들 수 있다.
useState, useReducer 포함 모든 훅에는 공통 규칙이 있다.
1. 컴포넌트 최상단에서만 호출하기
if, for, 함수 안의 함수 이런 곳에서는 호출 X
항상 컴포넌트 함수의 최상단에서 순서가 변하지 않게
2. 리액트 함수 컴포넌트나 커스텀 훅 내부에서만 호출
일반 자바스크립트 함수에서 Hooks 사용 X
- 빠르게 뜨는 서비스
첫 화면이 빨리 보이는지 (First Paint / LCP)
사용자가 클릭했을 때 바로바로 반응하는지 (TTI, 응답성)
- 가볍게 돌아가는 서비스
불필요한 렌더링이 없는지
자바스크립트 번들이 너무 크지 않은지
네트워크 요청이 최소한인지
- 사용자 환경에 맞는 서비스
느린 네트워크/저사양 기기에서도 적당히 쓸 만한지
- 서버 리소스도 효율적으로
필요 없는 API 호출 줄이기
캐싱, CDN 등 활용
즉
동일한 기능을 제공하되, 더 빠르고 더 가볍고, 더 적은 리소스로 동작하게 만드는 것 → 이걸 도와주는 것이 “최적화 도구들”
똑같은 props로 렌더링하면, 이전 결과를 재사용해서 다시 렌더링하지 않는다.
import { memo } from 'react';
const TodoItem = memo(function TodoItem({ todo, onToggle }) {
console.log('렌더링', todo.id);
return (
<li onClick={() => onToggle(todo.id)}>
{todo.text}
</li>
);
});
불필요한 렌더링을 줄이기 위해 사용한다.
같은 props가 들어오면, 이전 렌더링 결과를 재사용하고 다시 렌더링하지 않는다.
부모가 자주 렌더링되는데, 자식은 바뀔 일이 적을때 / 리스트 아이템 에 자주 사용한다.
“무거운 계산”을 캐싱해서, 의존성(deps)이 바뀔 때만 다시 계산한다.
const filteredTodos = useMemo(() => {
return todos.filter(todo => todo.text.includes(keyword));
}, [todos, keyword]);
정렬, 필터링, 큰 배열을 계산할 때 사용한다.
매 렌더마다 새로 만들어지는 함수를 “같은 레퍼런스”로 유지해준다.
const handleToggle = useCallback((id) => {
setTodos(todos =>
todos.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
)
);
}, []);
자식이 React.memo로 감싸져 있고, 그 자식에 콜백 함수를 props로 내려줄 때 유용하다.
“급한 업데이트(입력/클릭)”와 “덜 급한 업데이트(무거운 렌더링)”를 나눠 UI 버벅임을 줄인다.
useTransition 예시
const [isPending, startTransition] = useTransition();
function handleKeywordChange(e) {
const next = e.target.value;
setKeyword(next); // 입력 필드는 즉시 반응 (급한 업데이트)
startTransition(() => {
setFilteredTodos(filterBigList(allTodos, next)); // 무거운 작업
});
}
useDeferredValue 예시 (느리게 따라가는 값)
const deferredKeyword = useDeferredValue(keyword);
const filteredTodos = useMemo(
() => filterBigList(allTodos, deferredKeyword),
[allTodos, deferredKeyword]
);
지금 당장 필요 없는 컴포넌트는 나중에 네트워크로 받아오게 만들어서, 초기에 받는 JS 양을 줄인다.
const Chart = React.lazy(() => import('./Chart'));
function Page() {
return (
<Suspense fallback={<p>차트 불러오는 중...</p>}>
<Chart />
</Suspense>
);
}
Corner React.js
Editor: J
| [React.js] project 2[할일 관리] 앱 만들기 (Read: 할 일 리스트 렌더링하기 ~ Delete: 할 일 삭제하기) (0) | 2025.12.26 |
|---|---|
| [React.js] project 2[할일 관리] 앱 만들기 (프로젝트 준비하기 ~ Create: 할 일 추가하기) (0) | 2025.12.19 |
| [React.js] 리액트를 다루는 기술-8장.hooks (0) | 2025.11.21 |
| [React.js] project 1[카운터] 앱 만들기 ~ 6장. 라이프 사이클과 리액트 개발자 도구 (0) | 2025.11.14 |
| [React.js]5장.리액트의 기본 기능 다루기 (0) | 2025.11.07 |