JAN's History
React 학습하기 - State 관리하기 본문
https://ko.react.dev/learn/managing-state 를 보고 정리한 블로그입니다.
1. State를 통해 Input 다루기
- 컴포넌트의 다양한 시각적 State를 확인한다.
- 무엇이 State 변화를 트리거하는지 알아내고 변화의 흐름을 정의한다.
- 메모리의 state를 useState로 표현한다.
- 불필요한 state변수를 제거한다.
- 좋은 state 설계는 가능한 한 state를 단순화(합치기)해서 "불가능한 상태"를 없앤다.
- state 설정을 위해 이벤트 핸들러를 연결한다.
즉, 핵심 흐름 상태를 -> 선언하고 -> 이벤트로 상태를 업데이트하면 -> React가 상태에 맞춰 UI를 자동으로 보여준다
2. State 구조 선택하기
State 구조화 원칙
- 연관된 state 그룹화하기 - 두 개 이상의 state를 동시에 업데이트 한다면, 병합하자
- state 모순 피하기 - 불일치한 state가 있으면 실수가 생길 수 있으니 피하자
- 불필요한 state 피하기 - state를 통해 계산할 수 있다면 굳이 새로운 state를 만들지 말자
- state의 중복 피하기 - 중복된 데이터는 동기화가 어렵다.
- 깊게 중첩된 state 피하기 - 평탄한 방식으로 구현하자
단일 vs 다중 state 변수를 사용하는 경우
|
// 연관된 State는 단일 객체로 묶어라
//❌ 나쁜 예시
const [x, setX] = useState(0);
const [y, setY] = useState(0);
//✅ 좋은 예
const [position, setPosition] = useState({ x: 0, y: 0 });
|
원칙:
- x와 y처럼 항상 같이 변경되는 값이면, 객체로 묶자.
- 따로 관리하면 동기화 실패할 위험이 있다.
주의:
- 객체로 묶으면, 업데이트할 때 기존 값 복사(...) 해야 한다.
|
setPosition(prev => ({ ...prev, x: 100 }));
|
state를 구성할 때 피해야할 사항
|
// 연관된 State는 단일 객체로 묶어라
//❌ 나쁜 예시
const [isSending, setIsSending] = useState(false);
const [isSent, setIsSent] = useState(false);
//문제: isSending과 isSent가 동시에 true가 될 수도 있음 → 버그 위험
//✅ 좋은 예
const [status, setStatus] = useState('typing'); // 'typing' | 'sending' | 'sent'
|
요약:
- "같이 변하는" boolean 여러 개 대신,
- 하나의 명확한 status 값을 사용하자.
상태 구조의 일반적인 문제를 해결하는 방법
| 관련된 값이 서로 따로 관리됨 | 객체로 묶어라 |
| 여러 boolean으로 관리 | 하나의 status로 합쳐라 |
| 계산 가능한 값 저장 | 저장하지 말고 계산해라 |
| 중복된 데이터 저장 | ID만 저장해라 |
| 깊게 중첩된 구조 | 평탄화해서 관리해라 |
=> 가능한 한 단순하게, 하지만 필요 이상으로 단순하게는 하지말자
3. 컴포넌트 간 State 공유하기
State 끌어올리기를 통해 컴포넌트 간 state를 공유하는 방법

- 여러 컴포넌트가 같은 정보를 써야 할 때, 공통 부모 컴포넌트로 state를 옮긴다.
- 자식 컴포넌트들은 props로 값을 받고, props로 받은 핸들러를 통해 부모의 state를 바꾼다.
=> 상태를 부모한테 맡기고, 자식은 props만 받아서 행동한다.
제어 컴포넌트와 비제어 컴포넌트
| 상태 | 부모가 가진다 (props) | 자기 안에서 관리 (useState) |
| 제어 | 부모가 완전 제어 | 자기 마음대로 |
| 언제 | 폼 입력, 동기화 필요할 때 | 간단한 값만 필요할 때 |
4. State를 보존하고 초기화하기
React가 언제 state를 보존하고 언제 초기화하는지?
- 같은 위치 같은 컴포넌트일 경우 state가 유지된다.
- 같은 위치 다른 컴포넌트일 경우 state가 초기화된다.
- 컴포넌트를 DOM에서 제거하면 state도 삭제된다.
=> 위치(location) + 컴포넌트(type) 둘 다 같아야 state를 유지한다
어떻게 React가 컴포넌트의 state를 초기화하도록 강제할 수 있는지?
- 다른 위치에 렌더링 (if/else로 구분)
- key를 부여해서 강제로 새로운 컴포넌트로 인식시키기
|
{isPlayerA ? <Counter key="A" /> : <Counter key="B" />}
|
=> key를 다르게 주면 무조건 다른 컴포넌트로 취급해 state를 초기화한다.
key와 타입이 state 보존에 어떻게 영향을 주는지?
- 컴포넌트 타입이 다르면 state를 무조건 초기화한다.
- 같은 타입이지만 key가 다르면 state를 초기화한다 (다른 컴포넌트로 인식하기 때문)
- 같은 타입, 같은 key (or key없음) 이면 state를 보존한다.
=> type와 key 모두 중요, 하나라도 다르면 초기화된다.
5. State 로직을 reducer로 작성하기
reducer 함수란 무엇인가
- reducer 함수 = (현재 state, action) → 새로운 state 를 반환하는 순수 함수.
- 즉, 지금 state와 "어떤 일이 벌어졌는지(action)"를 받아서, "그 결과로 어떻게 변할지를" 계산하는 함수
useState에서 useReducer로 리팩토링 하는 방법
|
// useState일 때
setTasks([...tasks, newTask]);
// useReducer로 바꾸면
dispatch({ type: 'added', task: newTask });
|
- setState 대신 dispatch로 바꾼다
- 직접 state를 바꾸지 말고, dispatch(action) 을 호출해서 "무슨 일이 일어났는지"만 설명.
- reducer 함수를 따로 만든다.
- action을 보고 "다음 state가 뭐가 되어야 하는지"를 계산.
- useReducer(reducer, 초기값) 로 연결
- useState 대신 useReducer를 쓰고, [state, dispatch] 구조분해할당.
reducer를 언제 사용할 수 있는지
- 여러 state 업데이트가 복잡하게 얽혀있을 때 (예: 여러 input 조작, 복잡한 form 처리)
- 이벤트 핸들러마다 setState가 많아져서 컴포넌트가 복잡해질 때
- 상태 변화 패턴이 명확할 때 (예: '추가', '수정', '삭제' 등)
- 디버깅, 테스트를 쉽게 하고 싶을 때 (reducer 함수만 따로 테스트 가능)
- 관심사를 분리하고 싶을 때 (컴포넌트 → UI / reducer → 로직)
reducer를 잘 작성하는 방법
- 순수 함수로 만들기.
- 입력값(state, action)이 같으면 항상 결과(newState)도 같아야 한다.
- switch-case를 사용해 깔끔하게 분기하기.
- action.type은 의미 있는 이름으로!
- 각 action은 "한 가지 사용자 행동" 을 나타내야 한다.
- 예시: ADD_TASK, TOGGLE_TASK, RESET_FORM
- 필요하면 Immer를 사용해서 불변성을 쉽게 관리할 수 있다
=> 복잡한 state 관리는 useReducer로 통합해서, 더 읽기 쉽고 디버깅하기 쉬운 코드로 만들자
6. Contect를 사용해 데이터를 깊게 전달하기
”Prop drilling” 이란?
- 데이터를 사용하려는 컴포넌트까지 props를 중간중간 계속 전달하는 것.
- 불필요한 중간 컴포넌트들도 props를 받아야 해서 코드가 지저분해지고 관리가 어려워짐.
Context로 반복적인 prop 전달 대체하기
- Context를 쓰면 중간 단계를 생략하고 필요한 컴포넌트가 바로 데이터를 가져올 수 있다.
- 작업 순서
- createContext() 로 context 객체 만들기
- useContext() 로 자식 컴포넌트에서 읽기
- <Context.Provider value={값}> 로 상위 컴포넌트가 값 제공
Context의 일반적인 사용 사례
- UI 테마: 다크모드, 폰트 스타일 등 전체 앱에 적용되는 시각적 설정
- 현재 로그인 사용자: 어떤 컴포넌트에서도 현재 사용자 정보를 접근해야 할 때
- 라우팅 정보: 현재 URL 경로나 활성 링크 관리
- 글로벌 상태 관리: 복잡한 앱에서는 전역적으로 공유되는 상태를 다룰 때
Context의 일반적인 대안
- 그냥 props로 넘기기
=> 중간에 몇 번만 전달하는 거라면 props 전달이 더 깔끔하고 명확하다. - 컴포넌트 구조 개선
=> 중간 컴포넌트가 필요 없이 children 패턴(컴포넌트 합성)으로 구조를 단순화할 수 있다.
7. Reducer와 Context로 앱 확장하기
reducer와 context를 결합하는 방법
|
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
{children}
</TasksDispatchContext.Provider>
</TasksContext.Provider>
|
- useReducer로 state와 dispatch를 만든다.
- Context 2개 생성:
- TasksContext: state(tasks) 저장
- TasksDispatchContext: dispatch 함수 저장
- Provider를 이용해 트리 아래 모든 컴포넌트에 state와 dispatch를 제공한다.
state와 dispatch 함수를 prop으로 전달하지 않는 방법
|
const tasks = useContext(TasksContext);
const dispatch = useContext(TasksDispatchContext);
|
- props로 tasks, dispatch 를 일일이 넘기지 않고
- 필요한 컴포넌트 안에서 useContext 로 직접 가져온다
context와 state 로직을 별도의 파일에서 관리하는 방법
TasksContext.js 파일에 다 모아 관리:
|
export function useTasks() {
return useContext(TasksContext);
}
export function useTasksDispatch() {
return useContext(TasksDispatchContext);
}
|
- TasksContext와 TasksDispatchContext 생성
- TasksProvider 컴포넌트 만들어서 context 제공
- useTasks, useTasksDispatch 같은 커스텀 Hook도 같이 정의
'React' 카테고리의 다른 글
| 헤드리스 컴포넌트란? React에서 UI와 로직을 분리하는 방법 (0) | 2025.09.25 |
|---|---|
| React 학습하기 - 탈출구 (3) | 2025.08.08 |
| React 학습하기 - 상호작용 더하기 (3) | 2025.07.30 |
| React 학습하기 - UI 표현하기 (2) | 2025.07.26 |
| React 학습하기 - 빠르게 시작하기 (1) | 2025.07.21 |