효뚜르팝의 Dev log
[React 공식문서로 공부하기3] Managing State 본문
리액트 공식문서를 읽고 정리한 글입니다.
Managing State
https://react-ko.dev/learn/managing-state
1. Reacting to Input with State
선언형 UI와 명령형 UI의 차이점
- 명령형 UI : 방금 일어난 일에 따라 UI를 조작하기 위한 정확한 지침을 작성해야 함 ex) 폼에 무언가를 입력하면 "Submit" 버튼이 활성화 될 것이다. -> 더 복잡한 시스템에서는 관리하기가 기하급수적으로 어려워짐. 새로운 UI 요소나 인터랙션을 추가하려면 기존의 모든 코드를 주의깊게 살펴 버그의 발생 여부를 확인해야 함.
- 선언형 UI : 표시할 내용을 선언함. React에서 UI를 업데이트를 하는 방식.
참고) 선언형 프로그래밍 vs 명령형 프로그래밍
읽어보면 좋은 글!
https://ui.dev/imperative-vs-declarative-programming
UI를 선언적인 방식으로 생각하기 - UI를 React로 구현하는 과정
1. 컴포넌트의 다양한 시각적 상태를 식별한다.
- 사용자에게 표시될 수 있는 UI의 다양한 "상태"를 모두 시각화해야 한다. ex) 비어있음 : form의 "Submit"버튼 비활성화되어 있음
2. 상태 변화를 촉발하는 요소를 파악한다.
- 상태 변경을 촉발할 수있는 입력 : 사람의 입력(클릭, 입력 등) / 컴퓨터의 입력(네트워크 응답 도착, 로딩 등)
- state 변수를 설정해야 UI를 업데이트 할 수 있다. ex) text 입력을 변경하면 text box가 비어있는지 여부에 따라 비어있음 state에서 입력중state로 전환해야 함
3. useState를 사용하여 메모리의 상태를 표현한다.
- 필요한 state부터 컴포넌트의 시각적 상태를 표현할 것 ex) [isSubmitting, setIsSubmitting] = useState(false);
4. 비필수적인 state 변수를 제거한다.
- state가 모순을 야기하는지 ex) isTyping 과 isSubmitting이 같이 true가 되는 등 -> status라는 하나의 state로 관리 'typing', 'submitting', 'success'
- 다른 state 변수에 이미 같은 정보가 있는지 ex) isEmpty 를 제거하고 answer.length === 0 으로 표현
- 다른 state 변수를 뒤집으면 동일한 정보를 얻을 수 있는지 ex) isError를 제거하고 error !== null 로 표현
5. 이벤트 핸들러를 연결하여 state를 설정한다.
-> 명령형 ui 보다 훨씬 덜 취약함. 모든 상호작용을 state 변화로 표현하면 나중에 기존 상태를 깨지 않고도 새로운 시각적 상태를 도입할 수 있음. 또한 인터렉션 자체의 로직을 변경하지 않고도 각 state에 표시되어야 하는 항목을 변경할 수 있음.
2. Choosing the State Structure
state 구조화 원칙
1. 관련 state를 그룹화한다.
2. state의 모순을 피한다.
3. 불필요한 state를 피한다.
4. state 중복을 피한다.
5. 깊게 중첩된 state는 피한다.
3. Sharing State Between Components
제어 컴포넌트와 비제어 컴포넌트
- 비제어 컴포넌트 : 일반적으로 일부 로컬 state를 가진 컴포넌트로 부모 컴포넌트로부터 영향을 받지 않는 컴포넌트. 구성이 덜 필요하기 때문에 상위 컴포넌트 내에서 사용하기 쉽지만 함께 통합하려는 경우 유연성이 떨어짐.
SSOT (a single sourse of truth for each state, 각 state의 단일 진실 공급원)
- 단일 진실 공급원 : 각 고유한 state들에 대해 해당 state를 “소유”하는 컴포넌트를 선택하게 되는 원칙. 각 state마다 해당 정보를 소유하는 특정 컴포넌트가 있다는 뜻. 컴포넌트 간에 공유하는 state를 복제하는 대신 공통으로 공유하는 부모로 끌어올려서 필요한 자식에게 전달함.
4. Preserving and Resetting State
UI 트리
- 브라우저 : UI를 모델링하기 위해 트리구조 사용. DOM은 HTML요소를, CSSOM은 CSS요소를 나타냄. 접근성 트리도 있음.
- 리액트 : JSX로부터 UI를 만듬. 그런 다음 React DOM은 해당 UI 트리와 일치하도록 브라우저 DOM 엘리먼트를 업데이트 해야 함.
- state는 JSX 태그에 보관되지 않음. JSX를 넣은 트리 위치와 연관되어 있음.
- 같은 위치에 다른 컴포넌트를 렌더링하면 전체 하위 트리의 state가 재설정된다.
- 리렌더링 사이에 state를 유지하려면 트리의 구조가 일치해야 함.
- key를 사용해 React가 모든 컴포넌트를 구분하도록 할 수 있다.
{isPlayerA ? (
<Counter key="Taylor" person="Taylor" />
) : (
<Counter key="Sarah" person="Sarah" />
)}
-> key를 지정하면 React가 부모 내 순서가 아닌 key 자체를 위치의 일부로 사용하도록 지시함. 그렇기 때문에 JSX에서 같은 위치에 렌더링하더라도 React의 관점에서 보면 두 카운터는 서로 다른 카운터로 결과적으로 state를 공유하지 않는다.
-> key는 전역으로 고유하지 않음. 부모 내에서의 위치만 지정함.
5. Extracting State Logic into a Reducer
useState에서 useReducer로 마이그레이션하는 방법
- state를 설정하는 것에서 action들을 전달하는 것으로 변경하기
dispatch 함수에 넣어준 객체를 “action” 이라고 함.
function handleAddTask(text) {
// "action" object
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}
function handleChangeTask(task) {
dispatch({
type: 'changed',
task: task,
});
}
function handleDeleteTask(taskId) {
dispatch({
type: 'deleted',
id: taskId,
});
}
2. reducer 함수 작성하기
function tasksReducer(tasks, action) {
switch (action.type) {
case 'added': {
return [
...tasks,
{
id: action.id,
text: action.text,
done: false,
},
];
}
case 'changed': {
return tasks.map((t) => {
if (t.id === action.task.id) {
return action.task;
} else {
return t;
}
});
}
case 'deleted': {
return tasks.filter((t) => t.id !== action.id);
}
default: {
throw Error('Unknown action: ' + action.type);
}
}
}
3. 컴포넌트에서 reducer 사용하기
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks) // reducer함수, 초기값
- reducer는 반드시 순수해야 함. (state와 마찬가지로 렌더링 중에 실행이 된다.)
6. Passing Data Deeply with Context
Context : props 전달의 대안, Context로 data를 전달하는 방법
1. context를 생성한다.
export const MyContext = createContext(defaultValue)
2. 데이터가 필요한 컴포넌트에서 해당 context를 사용한다.
useContext(MyContext) 훅에 전달하여 깊이에 상관없이 모든 하위 컴포넌트에서 읽을 수 있도록 함
3. 데이터를 지정하는 컴포넌트에서 해당 context를 제공한다.
context provider로 감싸서 context를 제공한다.
자식 컴포넌트를 <MyContext.Provider value={...}>로 감싸서 부모로부터 제공받음.
Context를 사용하기 적절한 상황은?
props를 몇 단계 깊이 전달해야 한다고 해서 해당 정보를 context에 넣어야 한다는 의미는 아니다.
먼저 props 전달을 하거나 혹은 컴포넌트를 추출하고 JSX를 childeren으로 전달하여 component 레이어의 수를 줄여본다.
-> 이 접근 방식이 적합하지 않을 때 context 사용을 고려
Context의 사용 사례
- 테마 ex) 다크 모드
- 현재 계정
- 라우팅
- state 관리
7. Scaling Up with Reducer and Context
Reducer와 context를 결합하는 방법
1. Context를 생성한다.
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
2. State과 dispatch 함수를 context에 넣는다.
export default function TaskApp() {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
// ...
return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
...
</TasksDispatchContext.Provider>
</TasksContext.Provider>
);
}
3. 트리 안에서 context를 사용한다.
const dispatch = useContext(TasksDispatchContext);
- reducer와 context를 모두 하나의 파일에 작성하면 컴포넌트들을 조금 더 정리할 수 있음.
- 바로 사용할 수 있도록 useTasks와 useTasksDispatch 같은 사용자 Hook을 내보낼 수 있음.
참고 ) context api는 상태 관리 라이브러리가 아니라 의존성을 주입해주는 도구이다. (state, reducer만이 상태관리를 해줌)
읽어보면 좋은 글!
'TIL' 카테고리의 다른 글
| [React 공식문서로 공부하기5] useEffect와 useLayoutEffect (3) | 2024.03.17 |
|---|---|
| [React 공식문서로 공부하기4] Escape Hatches 탈출구 (5) | 2024.03.04 |
| [React 공식문서로 공부하기2] Adding Interactivity 상호작용 추가하기 (1) | 2024.02.20 |
| [React 공식문서로 공부하기] Describe the UI (0) | 2024.02.18 |
| PoC를 진행할 때 유의할 점에 대해서 (3) | 2024.02.04 |