[React 공식문서 이해하기] Managing State
Choosing the State Structure
✅ Avoid deeply nested state 깊게 중첩된 state는 피하기
handleComplete 함수 해석
- 완료버튼을 클릭할 때 발생되는 함수로 nextParent에 클릭한 childId가 제외된 childIds가 새로 들어감
- 새로 만들어진 배열이 setPlan에 들어가 화면에 보여지게 됨
- .filter(id => id!==childId) : id들이 childId와 같지 않은 것만 childIds에 넣어주도록 함
--> 클릭한 childId가 2일 경우 [1,2,3,4,5] 가 있다면 filter 비교하여 [1,3,4,5]만 childIds에 들어가게 됨
Sharing State Between Components
✅ Lifting state up by example : state 끌어올리기
하나의 state 사용하지만 각 패널에 적용됨
위에 Panel 함수 정의하고 아래 Accordion 함수에 각 Panel을 불러와 적용함
결론적으로 한 패널의 버튼을 눌러도 다른 패널에 영향을 주지않고 독립적으로 동작
------------------------------------------------------------------------------
이 때 state를 아래의 단계에 거쳐 부모로 끌어올려 공유 할 수 있음
1. 자식 컴포넌트에서 state를 제거
- isActive state 삭제 후 props로 isActive를 받음
2. 공통 부모 컴포넌트에 하드 코딩된 데이터를 전달
- Accordion 부모 컴포넌트에 isActive={true} 데이터 넣어줌
3. 공통 부모 컴포넌트에 state를 추가하고 이벤트 핸들러와 함께 전달
- state값 추가 : 어떤 패널이 활성화된 패널인지 추적해야하므로 boolean 값 대신 숫자값을 넣어줌
- 이벤트 핸들러로 state 변경
⭐️ 이 과정을 거치면 state 끌어올리기 성공!
--> state를 공통 부모 컴포넌트로 옮기면 두 패널을 조정할 수 있게 됨
✅ A single source of truth for each state 단일 진실 공급원
- 모든 state가 한 곳에 있다는 뜻이 아니라, 각 state마다 해당 정보를 소유하는 특정 컴포넌트가 있다는 뜻
- 컴포넌트마다 공유하는 state들을 각자 다 생성하는 것이 아닌, 부모(단일 책임)로 state를 끌어올려 자식에게 전달하는 것을 말함
Preserving and resetting state
✅ 컴포넌트 함수 정의 중첩하지 않도록 함
위 코드는 MyComponent 함수 안에 MyTextField 함수 정의가 중첩되어 있음
➡️ 텍스트 입력 후 버튼 클릭 시 텍스트 값이 초기화되는 문제점이 발생
➡️ MyComponent 렌더링 할 때마다 MyTextField 렌더링되면서 모든 state를 초기화시킴
⭐️ 컴포넌트 함수를 최상위 수준에서 선언하고 정의 중첩시키지 않도록 함! (아래코드 참고)
export default function MyComponent() {
const [counter, setCounter] = useState(0);
const [text, setText] = useState('');
return (
<>
<MyTextField text={text} setText={setText} />
<button onClick={() => {
setCounter(counter + 1);
}}>Clicked {counter} times</button>
</>
);
}
function MyTextField({ text, setText }) {
function handleTextChange(e) {
setText(e.target.value);
}
return (
<input
value={text}
onChange={handleTextChange}
/>
);
}
✅ key의 중요성
- 각 엘리먼트에 고유성을 부여함, 여기에서 말하는 고유성이란 이름이 똑같은 컴포넌트들 각각 항목을 변경, 추가, 삭제 식별이 가능한 것
- key가 없다면 변화된 부분에 대해 렌더링을 하기 위해 전체를 다 훑고 바뀐 부분에 대해 리렌더링함 -> 비효율적
- key가 있다면 키값만 보고 변화된 부분을 인지하여 리렌더링함 -> 효율적
- key로 state를 재설정하는 것은 form을 다룰 때 특히 유용
Extracting State Logic into a Reducer
✅ useState ➡️ useReducer 마이그레이션 3단계
1. state를 설정하는 것에서 action들을 전달하는 것으로 변경
2. reducer 함수 작성하기 - switch문 사용하는 것이 일반적
3. 컴포넌트에서 reducer 사용하기
⭐️ 이벤트 핸들러는 action을 전달하여 무슨일이 일어났는지만 지정하고 reducer함수는 action에 대한 응답으로 state가 어떻게 변경되는지를 결정함!!
✅ useState & useReducer 비교
- code size : 일반적으로 useState 사용하면 미리 작성해야하는 코드가 줄어듦, 하지만 많은 이벤트 핸들러가 비슷한 방식으로 state를 업데이트 하는 경우에는 useReducer를 사용하는 것이 코드를 줄이는데 도움이 됨
- readability : useState로 간단한 state 업데이트 하는 경우 가독성 좋음, state 구조가 복잡해지면 useReducer를 사용하면 업데이트 로직이 어떻게 동작하는지와 이벤트 핸들러를 통해 무엇이 일어났는지를 깔끔하게 분리할 수 있음
- debugging : useState 사용하면 버그가 있는 경우 어느 state가 어디서 잘못 설정되었는지 왜 그런지 알기 어려움, useReducer 사용하면 reducer에 콘솔로그 추가하여 모든 state 업데이트와 왜 버그가 발생했는지 확인 가능
- Testing : reducer는 컴포넌트에 의존하지 않는 순수한 함수로 별도로 분리해서 내보내거나 테스트 할 수 있음
- Personal perference : 어느 걸 사용해도 됨! 개인 취향차이임
✅ immer library
- 리액트에서 배열이나 객체를 업데이트할 때 직접 수정하지 않고 불변성을 지켜주면서 업데이트를 해야하는데, 이 때 쉽게 불변성을 유지하면서 업데이트를 도와주는 라이브러리
- 기존 객체를 직접 수정하는 것이 아닌 객체의 복사본을 만들어 변경작업을 수행하고, 이를 기존 객체에 적용하는 방식으로 업데이트함
- reducer를 간결하게 만들 수 있음
- useImmerReducer 사용하면 push 또는 arr[i]= 할당으로 state 변이할 수 있음
- reducer는 순수해야하므로 state를 변이하면 안됨, Immer는 안전하게 변이할 수 있는 특별한 draft 객체를 제공, draft로 state의 복사본을 생성
Passing data deeply with context
✅ useContext
- 상태(state)를 전역적으로 관리하기 위한 방법 중 하나로 context가 없다면 props가 필요할 때 하위 컴포넌트로 계속 전달해야함, 그렇게 되면 props drilling이라는 문제점이 발생
- createContext 메소드를 사용하여 생성된 객체로, 이를 이용하여 어떤 컴포넌트에서든 값 공유 가능
- provider, consumer 를 제공하여, provider로 값을 설정하고, consumer로 값을 가져와 사용함
⭐️ 컴포넌트 간의 데이터 전달이 간편, 효율적으로 이루어짐, 하지만 너무 많이 사용할 경우 의존성이 높아질 수 있으므로 필요한 경우에만 사용하는 걸 지향함
1. createContext 로 Context 객체 생성
createContext 함수는 0을 인자로 받음
0은 Provider를 통해 값을 전달하지 않았을 때 기본값으로 사용
컴포넌트에서 사용할 수 있도록 export 해주도록 함
2. useContext로 context값 사용
useContext는 react에게 Heading 컴포넌트가 LevelContext 읽기를 원한다고 알려줌
useContext hook을 사용하여 consumer를 간단하게 사용할 수 있음
useContext로 context 객체를 가져온 후 이를 사용하여 값 사용이 가능함
3. Provider 로 원하는 context 전달
LevelContext.Provider로 감싸 LevelContext 제공
LevelContext.Provider 를 사용하여 하위 컴포넌트에 값을 전달할 수 있음
이 때 value prop에 전달할 값을 넣음 -> provider 컴포넌트 내부에 있는 모든 하위 컴포넌트(children)에서 이 값을 사용할 수 있음
⭐️ 정리
= createContext 함수로 컨텍스트를 생성한 후, Provider 컴포넌트를 사용하여 값을 제공하고, useContext를 사용하여 값을 소비
1. level prop을 <Section>에 전달함
2. Section은 section 의 children을 <LevelContext.Provider value={}>로 감쌈
3. Heading은 useContext(LevelContext)를 사용하여 위의 LevelContext값에 가장 가까운 값을 요청
-> 결과적으로 보면 LevelContext.js에 create 해줄 때 초기값으로 level 0으로 설정함
App.js에는 더이상 level prop을 컴포넌트로 전달해주지 않아도 됨
-> Section.js 에서 또한 level을 prop으로 전달하지 않고 useContext사용하여 가져오게 됨 (초기값 0)
LevelContext.provider를 통해 값을 사용하도록 하고 value안에는 들어온 level 초기값에 +1을 하여 1부터 시작
그 뒤로 level+1.. 쭉쭉 되면서 level={2} 이런식으로 명시하지 않아도 자동으로 다음 숫자들이 들어가게 됨
-> Section.js 로 부터 들어온 level 값을 사용하여 Heading.js에서 스위치문을 통해 화면에 출력됨
✅ Context는 중간 컴포넌트들을 통과힘
- 일반적으로 React 컴포넌트 트리에서 데이터를 전달하기 위해서는 props를 사용하여 상위 컴포넌트에서 하위 컴포넌트로 값을 전달
- 하지만 컴포넌트 트리의 깊은 곳에 위치한 컴포넌트에서 상위 컴포넌트로부터 전달된 값을 필요로 할 때, 중간 컴포넌트를 모두 거쳐야 하는 번거로움이 발생
- useContext를 사용하면 이러한 중간 컴포넌트를 통과하지 않고도 값을 직접 접근가능
Scaling up with reducer and context
✅ reducer + context 결합
- 수십개, 수백개의 컴포넌트에 props를 전달하는 것은 상당히 비효율적이므로 prop 대신에 작업 상태와 디스패치 함수를 모두 컨텍스트에 넣음
1. context 생성
useReducer hook은 현재 작업과 이를 업데이트할 수 있는 dispatch 함수를 반환함
TasksContext : 현재 tasks list 제공
TasksDispatchContext : 컴포넌트가 작업을 디스패치 할 수 있는 함수 제공
두 context는 나중에 다른 파일에서 가져올 수 있도록 별도의 파일에서 내보냄 (export)
2. state 와 dispatch함수를 context에 넣기
3. 트리 안에서 context 사용
tasks list나 이벤트 핸들러를 트리 아래로 전달할 필요가 없음
대신 필요한 컴포넌트에서는 TasksContext에서 task list를 읽을 수 있음
⭐️ state 와 state를 관리하는 useReducer는 여전히 최상위 컴포넌트에 있음, 그러나 tasks, dispatch는 하위 트리 컴포넌트 어디서나 context를 불러와서 사용할 수 있음
좌측의 코드를 우측처럼 정리해서 사용할 수 있음!