https://react.dev/learn/queueing-a-series-of-state-updates를 읽고 정리한다.

 

state 변수를 설정하면 다른 render를 대기열에 추가한다.

때때로 다음 render를 대기열에 추가하기 전에 많은 연산을 수행하고 싶을 수도 있다. 이때 리액트가 state update를 일괄 처리하는 방법을 이해하는 것이 도움이 될 것이다.

 

리액트는 state update를 일괄 처리한다.

각 render의 state 값을 고정되어있다. 그래서 아래와 같이 state setter 함수를 여러 번 호출을 하더라도 현재 상태 값은 변하지 않는다.

 

리액트는 이벤트 핸들러 내 모든 코드가 실행될 때까지 기다린 후 state update가 처리된다. 이는 리렌더가 모든 setNumber() 호출 후에  re-render가 일어나는 이유이다.

 

이는 레스토랑에서 웨이터가 주문을 받을 때를 떠올려보자. 웨이터는 첫 번째 요리를 언급할 때 바로 주방에 가지 않는다. 대신, 주문이 끝나고 이를 변경할 수 있으며 테이블에 다른 사람의 주문도 받을 수 있다.

 

이렇게 하면 아주 많은 re-render를 트리거하지 않고, 여러 컴포넌트의 여러 state 변수를 업데이트할 수 있다. 그러나 이는 또한 이벤트 핸들러와 그 안의 모든 코드가 완료될 때까지 UI를 업데이트할 수 없다는 것을 의미한다. batching(일괄 처리)라고 알려진 이 행동은 리액트 앱을 더 빠르게 만든다. 이는 또한 일부 변수만 업데이트되는 혼란스러운 "중도-멈춤" 렌더를 다루는 것을 피한다.

 

리액트는 클릭과 같은 다양한 의도적인 이벤트를 일괄 처리하지 않는다. - 각 클릭은 별도로 다뤄진다. 나머지는 리액트가 일반적으로 안전한 경우에만 일괄 처리를 수행하므로 안심해라. 예를 들어, 첫 번째 버튼을 클릭이 form을 비활성화하면, 두 번째 클릭은 다시 제출할 수 없다는 것을 확실히 한다.

 

다음 render 전에 동일한 state 변수를 여러 번 업데이트

이는 자주 사용되는 방법은 아니지만, 아래의 방법으로 next render 전에 같은 state 변수를 여러 번 업데이트할 수 있다.

setNumber(n => n + 1)

  • 큐의 이전 값을 기준으로 next state를 계산하는 함수를 전달
  • 이는 리액트에게 state를 단지 교체(replace)하는 대신에 "state 값으로 무언가를 수행하라"라고 말한다.
    • 교체 : setNumber(number + 1)

n => n + 1updater function이라고 불린다. 이 함수를 state setter에 전달하는 시점은 아래와 같다.

  1. 리액트는 이벤트 핸들러에 있는 모든 코드가 실행된 후에 처리되기 위해 이 함수를 대기열에 추가한다.
  2. next render 동안, 리액트는 대기열을 거치고 최종 업데이트된 state를 제공한다.
setNumber(n => n + 1);
setNumber(n => n + 1);
setNumber(n => n + 1);

이벤트 핸들러를 실행하는 동안 이 줄의 코드는 리액트에서 아래와 같이 동작한다.

  1. setNumber(n => n + 1)`: `n => n + 1함수이니까 리액트는 이 함수를 대기열에 추가한다.
  2. setNumber(n => n + 1)`: `n => n + 1은 함수이니까 리액트는 이 함수를 대기열에 추가한다.
  3. setNumber(n => n + 1)`: `n => n + 1은 함수이니까 리액트는 이 함수를 대기열에 추가한다.

next render 동안 useState를 호출하면, 리액트는 대기열을 거친다. 이전의 number state가 0이고, 이는 첫 번째 updater function의 n 인수로 전달한다. 그 후 리액트는 이전 updater function의 반환 값을 다음 updater의 n으로 전달한다.

queued update n returns
n => n + 1 0 0 + 1 = 1
n => n + 1 1 1 + 1 = 2
n => n + 1 2 2 + 1 = 3

 

리액트는 마지막 결과로 3을 저장하고 useState로부터 이를 반환한다.

이것이 예제에서 "+3" 버튼을 클릭했을 때 올바르게 3의 값만큼 증가된 이유이다.

state를 교체한 후 state를 업데이트하면 어떻게 될까

아래 이벤트 핸들러를 가질 때 next render에서 number는 어떻게 될까?

<button onClick={() => {
  setNumber(number + 5);
  setNumber(n => n + 1);
}}>

이 이벤트 핸들러는 리액트에서 아래처럼 동작한다.

  1. setNumber(number + 5) : number는 0이므로 setNumber(0 + 5)가 된다. 리액트는 대기열에 "5로 교체"를 추가한다.
  2. setNumber(n => n + 1) : n => n + 1은 updater function이다. 리액트는 대기열에 이 함수를 추가한다.

next render 동안, 리액트는 아래의 상태 대기열을 거쳐간다.

queued update n returns
"5로 교체" 0 (unused) 5
n => n + 1 5 5 + 1 = 6

리액트는 최종 결과로 6을 저장하고 useState로부터 리턴한다.

setState(x)는 실제로 setState(n => x)로 동작하지만, n이 사용되지 않는다.

state가 업데이트된 후 state를 교체하면 어떻게 될까

아래 이벤트 핸들러를 가질 때 next render에서 number는 어떻게 될까?

<button onClick={() => {
  setNumber(number + 5);
  setNumber(n => n + 1);
  setNumber(42);
}}>

이 이벤트 핸들러는 리액트에서 아래처럼 동작한다.

  1. setNumber(number + 5) : number는 0이므로 setNumber(0 + 5)가 된다. 리액트는 대기열에 "5로 교체"를 추가한다.
  2. setNumber(n => n + 1) : n => n + 1은 updater function이다. 리액트는 대기열에 이 함수를 추가한다.
  3. setNumber(42) : 리액트는 대기열에 "42로 교체"를 추가한다.

next render 동안, 리액트는 아래의 상태 대기열을 거쳐간다.

queued update n returns
"5로 교체" 0 (unused) 5
n => n + 1 5 5 + 1 = 6
"42로 교체" 6 (unused) 42

리액트는 최종 결과로 42를 저장하고 useState로부터 리턴한다.

 

정리하면, setNumber state setter에 전달되는 것이 무엇인지에 따라 다르게 생각해야 한다.

  • updater function(n => n + 1) : 대기열에 추가된다.
  • 그 외 다른 값(5)은 대기열에 "5로 교체"가 추가되며, 이미 대기열 있던 것은 무시된다.

이벤트 핸들러가 실행이 완료된 후, 리액트는 re-render를 트리거한다. re-render 동안, 리액트는 대기열을 처리할 것이다. updater function은 rendering동안 실행하며, 이 함수는 pure 해야 하며 오직 결과만 리턴해야 한다.

  • 내부에서 set state를 하지 마라
  • 다른 side effect를 실행하지 마라

Strict Mode에서 리액트는 실수를 찾는데 도움을 주기 위해 각 updater function 별로 2번 실행할 것이다. 이때 두 번째 결과는 버린다.

 

네이밍 컨벤션

state 변수의 첫 글자로 updater function 인자의 이름을 지정하는 것이 일반적이다.

setEnabled(e => !e);
setLastName(ln => ln.reverse());
setFriendCount(fc => fc * 2);

더 긴 코드를 선호한다면, state의 전체 이름은 반복하는 거나, prefix를 사용하는 것이 일반적이다.

setEnabled(enabled => !enabled);
setEnabled(prevEnabled => !prevEnabled)

 

https://react.dev/learn/referencing-values-with-refs를 읽고 정리한다.

 

컴포넌트가 어떤 정보를 "기억"해야 하지만, 리 렌더링이 트리거 되지 않기를 원한다면, `ref`를 사용하면 된다.

 

React의 `useRef` Hook을 이용해 컴포넌트에 `ref`를 추가할 수 있다.

  • 컴포넌트 내부에서 `useRef` 훅을 호출할 수 있으며, 오직 argument를 통해 초기값을 설정할 수 있다.
  • `ref.current` 프로퍼티를 통해 ref의 현재 값에 접근할 수 있다.
    • 이 값은 언제든 변할 수 있다. 즉, 읽고 쓰기가 둘 다 가능하다.
  • 리액트가 추적하지 않는 값이다.
    • 단방향 데이트 흐름을 가진 리액트로부터의 탈출 수단(escape hatch)이 되는 방법이다.
  • ref는 읽고 수정할 수 있는 `current` 프로퍼티를 가진 js Object 객체이다.
    • 어떤 값도 사용할 수 있다.
  • state처럼 리 렌더링 간에 값이 유지된다. 하지만 리 렌더링을 트리거하지는 않는다.

ref와 state의 차이

ref state
`useRef(initialValue)`의 반환 값
=> `{ current: initialValue }`
`useState(initialValue)`의 반환 값
=> `[value, setValue]` : 현재 값과 state setter 함수
변경되도 리렌더링을 트리거하지 않음 변경되면 리렌더링이 트리거됨
mutable(변경가능한 값)
- 렌더링 프로세스 외부에서 `current`의 값을 수정하고 업데이트 가능하다.
immutable(불변 값)
- state setter 함수를 사용해 state 변수를 수정하고, 리렌더링 대기열에 추가된다.
렌더링할 때(in JSX) `current`값을 읽거나 쓸 수 없다. 언제든 state를 읽을 수 있다.
하지만, 각 렌더는 고유의 변경되지않는 state의 스냅샷을 가지고 있다.
내부에서 useRef는 어떻게 동작하는 가?

React 내부에서 아래처럼 구현된다고 상상할 수 있다.
// Inside of React
function useRef(initialValue) {
  const [ref, unused] = useState({ current: initialValue });
  return ref;
}​

첫 렌더에서, `useRef`는 `{ current: initialValue }`를 리턴한다. 이 객체는 react로부터 저장되므로, 다음 렌더링 때도 동일한 객체가 반환된다.
`useRef`는 항상 동일한 객체를 반환해야 하기 때문에 state setter 함수는 불필요하다.

`useRef`는 React에서 내장되어 제공된다. 하지만 위와 같이 setter가 없는 일반 state 변수로 생각할 수도 있다.

 

ref를 사용하는 곳

컴포넌트의 외부 API와 통신과 같은 React 외부에서 무언가를 해야할 때 ref를 사용한다.

  • React 외부에서 무언가를 해야할 때
    • 외부 API와 통신할 때 ex) 컴포넌트의 모양에 영향을 주지 않는 브라우저 API
  • timeout ID를 저장할 때
  • DOM element 조작하거나 저장할 때
  • JSX 계산이 필요없는 다른 객체를 저장할 때

만약 컴포넌트가 값을 저장해야 하는 데, 렌더링 로직에 영향을 끼치지 않는다면 ref를 사용한다.

ref를 위한 Best practices

아래의 원칙을 따르면 컴포넌트를 보다 예측 가능하게 만들 수 있다.

  • ref를 탈출구로만 사용하기. ref는 외부 시스템과 browser API와 함께 작업할 때 유용하다. 앱의 많은 로직과 데이터 플로우가 ref에 의존적이라면, 접근 방법을 다시 생각해봐야 한다.
  • 렌더링 도중 ref.current를 읽고 쓰지 말기. 렌더링 할 때 여러 정보가 필요하다면, ref 대신 state 사용해라. 리액트는 ref.current가 변경될 때를 모르기 때문에 렌더링 동안 ref.current를 읽는 것은 컴포넌트의 행동이 예측하기 어렵게 만든다.
    • 예외적으로 아래 코드처럼 첫 번째 렌더에서 한 번만 ref를 설정하는 동작은 가능하다.
if(!ref.current) {
	ref.current = new Thing();
}

 

리액트의 state에서 한계는 refs에 적용되지 않는다. 예를 들어 state는 모든 렌더링을 위한 스냅숏인 것처럼 행동하고 동기적으로 업데이트되지 않는다. 그러나 ref의 current value는 변경할 수 있는 값이어서 즉시 값을 변경할 수 있다.

이에 대한 예제로 Challeges > 4.Read the latest state를 참고할 수 있다.

  • 같은 값(input text)에 대해 state, ref 모두 사용한다.
    • state 사용 이유 : 렌더링할 때 현재 input text 값을 사용
    • ref 사용 이유 : timeout과 같은 비동기 연산 최신 input text 값을 읽기 위해
      • 이 때, 최신 state는 읽을 수 없기때문에 ref를 사용해 최신 값을 저장하고 사용

ret 자체가 일반적인 JavaScript 객체이기 때문에, 객체처럼 동작한다.

또한 ref를 사용할 때 mutation을 피하는 것에 대해 걱정할 필요가 없다. 변경하는 객체를 렌더 동안 사용하지 않는 한, 리액트는 ref와 내부 콘텐츠에 무엇을 하든 상관하지 않는다.

 

ref와 DOM

ref는 어떤 값이든 가리킬 수 있다. 그러나, ref에서 가장 일반적인 유스케이스는 DOM 요소를 접근하는 것이다. 예를 들어, input을 프로그램에 따라 포커스를 주고 싶을 때 편리하다. <div ref={myRef}>처럼 JSX의 ref 속성에 ref를 전달할 때, 리액트는 해당하는 DOM 요소를 myRef.current에 넣는다. (더 알아보려면 ref를 사용해 DOM 조작하기를 읽기)

https://react.dev/learn/scaling-up-with-reducer-and-context를 정리한다.

 

Reducer와 Context를 같이 사용하여 복잡한 화면의 상태를 관리할 수 있다.

  • Reducer를 사용하면 컴포넌트의 state 업데이트 logic을 통합할 수 있다.
  • context를 사용하면 정보를 다른 컴포넌트로 deep하게 전달할 수 있다.

앱이 성장함에 따라 이와 같은 context-reducer 쌍이 많이 있을 수 있다. 이는 트리의 깊숙한 데이터에 액세스하려고 할 때마다 너무 많은 작업 없이 앱을 확장하고 state를 위로 올릴 수 있는 강력한 방법이다.

Reducer와 Context 같이 사용하기

Reducer 소개 단원 예제에서 state는 reducer로 관리되고 있다. reducer 함수는 모든 state update logic을 포함한다.

reducer는 이벤트 핸들러를 짧고 간결하게 유지하는 데 도움이 된다. 그러나 앱이 성장함에 따라 다른 어려움이 생길 수 있다. "현재 tasks state와 dispatch function은 최상위 컴포넌트(TaskApp)에서만 사용할 수 있다".

다른 컴포넌트가 task 목록을 읽거나 변경할 수 있도록 하려면 현재 상태와 이를 변경하는 이벤트 핸들러를 props을 사용해 명시적으로 전달해야한다.

// TaskApp은 task 목록과 이벤트 핸들러를 TaskList로 전달한다.
<TaskList
  tasks={tasks}
  onChangeTask={handleChangeTask}
  onDeleteTask={handleDeleteTask}
/>

// TaskList는 Task에 이벤트 핸들러를 전달한다.
<Task
  task={task}
  onChange={onChangeTask}
  onDelete={onDeleteTask}
/>

위의 예제는 잘 돌아가지만 중간에 많은 수의 컴포넌트가 있을 경우, 모든 state와 function을 아래로 전달해야하고 이는 별로다!

이를 개선하기 위해 tasks state와 dispatch함수를 모두 컨텍스트에 넣을 수 있다.  이 방법은 트리에서 TaskApp아래의 모든 컴포넌트가 반복적인 "prop drilling" 없이 tasks을 읽고 action을 dispatch할 수 있습니다.

다음은 리듀서를 컨텍스트와 결합하는 방법입니다.

 

1. context를 만든다.

`useReducer` Hook은 현재 `tasks`와 이를 업데이트 할 수 있는 `dispatch` 함수를 반환한다.

const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

트리 아래로 반환된 값을 전달하기 위해, 두개의 분리된 context를 생성 후 다른 파일에서 import할 수 있게 export한다.

  • `TasksContext` : 현재 task 목록을 제공
  • `TastsDispatchContext` : 컴포넌트가 dispatch action을 할 수 있는 함수를 제공
// TasksContext.js
import { createContext } from 'react';

export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);

현재 두 context는 default value로 `null`을 전달하고 있다. 실제 값은 `TaskApp` 컴포넌트에게 제공받을 것이다.

2. statue와 dispatch를 ​​context에 넣는다.

`TaskApp` 컴포넌트에서 두 context를 import한 후, `useReducer()`에서 반환된 `tasks`와 `dispatch`를 context에 제공한다.

import { TasksContext, TasksDispatchContext } from './TasksContext.js';	// *

export default function TaskApp() {
  const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
  // ...
  return (
    <TasksContext.Provider value={tasks}>	// *
      <TasksDispatchContext.Provider value={dispatch}>	// *
        ...
      </TasksDispatchContext.Provider>
    </TasksContext.Provider>
  );
}

지금은 두 곳(props, context)에서 정보를 전달하고 있다. 다음 차례에 props를 제거하자. 

3. 트리의 아무 곳에서나 context를 사용한다.

이제 task 목록과 이벤트 핸들러를 아래로 내리지 않아도 된다.

대신 `TaskContext`를 통해 어떤 컴포넌트든 task 목록이 필요하면 읽을 수 있다.

const tasks = useContext(TasksContext);

또한, `TaskDispatchContext`를 통해 task 목록을 업데이트 하기위해 어떤 컴포넌트든 `dispatch` 함수를 읽을 수 있다. 

const dispatch = useContext(TasksDispatchContext);

위의 모든 단계를 적용하면 아래와 같다.

state는 여전히  top-level(TaskApp) 컴포넌트에 존재하고, `useReducer`로 관리된다. 그러나 `tasks`, `dispatch`는 context를 사용해 아래의 모든 컴포넌트에서 사용할 수 있다.

 

모든 것을 하나의 파일로 옮기기

이렇게 할 필요는 없지만 reducer와 context를 단일 파일로 이동하여 컴포넌트를 더 깔끔하게 정리할 수 있습니다.

현재 TasksContext.js는 두 개의 컨텍스트 선언만 포함하고 있다.

import { createContext } from 'react';

export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);

reducer를 동일한 파일로 이동한 후, 새로운 `TasksProvider` 컴포넌트를 선언한다. 이 컴포넌트는 모든 조각을 함께 묶는다.

  1. reducer로 상태를 관리한다.
  2. 아래 구성 요소에 두 context를 모두 제공한다.
  3. JSX 를 전달할 수 있도록 prop으로 children를 사용한다.
export function TasksProvider({ children }) {
  const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

  return (
    <TasksContext.Provider value={tasks}>
      <TasksDispatchContext.Provider value={dispatch}>
        {children}
      </TasksDispatchContext.Provider>
    </TasksContext.Provider>
  );
}

이러면 `TaskApp` 컴포넌트에 있는 복잡성과 wiring을 제거할 수 있다.(아래에서 App.js, TasksContext.js를 확인해보라)

 

추가로, TasksContext.js에서 context를 사용하는 함수를 export할 수 있다.

export function useTasks() {
  return useContext(TasksContext);
}

export function useTasksDispatch() {
  return useContext(TasksDispatchContext);
}

컴포넌트가 context를 읽어야할 때, 이 함수를 사용할 수 있다.

const tasks = useTasks();
const dispatch = useTasksDispatch();

동작이 절대 변경되지 않지만, 나중에 이런 context를 더 분할하거나 일부 logic를 추가할 수 있다. 이제 모든 context 및 reducer wiring이 TasksContext.js에 있다. 이렇게 하면 데이터를 가져오는 위치보다 표시되는 내용에 집중하여 컴포넌트를 깨끗하고 깔끔하게 유지한다.(TaskList 참고)

결과적으로 아래처럼 분리해서 생각할 수 있다.

  • `TasksProvider` : tasks를 다루는 방법을 아는 part of the screen
  • `useTasks` : tasks를 읽는 방법
  • `useTasksDispatch` : tasks를 변경하는 방법
`userTask`, `userTasksDispatch` 함수는 커스텀 훅이라고 불린다. 이름이 use로 시작하는 함수는 커스텀 훅이다. 이렇게 하면 내부에서 다른 `useContext`와 같은 다른 훅을 사용할 수 있다.

https://react.dev/learn/passing-data-deeply-with-context를 읽고 정리한다.

 

보통 부모 컴포넌트에서 자식 컴포넌트로 props를 통해 정보를 전달한다. 하지만 그 중간에 많은 컴포넌트를 거쳐야 하는 경우, 코드가 장황해지고 불편해진다.

Context를 사용하면 부모 컴포넌트가 props를 통해 명시적으로 전달하지 않고도 하위 트리의 모든 컴포넌트에서 일부 정보를 사용할 수 있도록 한다.

Props 전달할때의 문제 : Prop driling

tree를 통해 일부 prop을 깊게 전달해야하거나 많은 컴포넌트에 동일한 prop이 필요한 경우, 상태를 높이 올려서(lifting state up) 해결한다. 이 방법을 "prop driling"이라고 한다. 이럴 경우, 반복적으로 props를 전달해야 한다는 단점이 있다.

prop driling을 사용하면 prop을 실제로 사용하지 않는 중간 컴포넌트도 거쳐야한다.

Context : 반복적인 prop 전달을 대체하는 방법

context를 사용하면 상위 컴포넌트가 그 아래의 전체 트리에 데이터를 제공할 수 있다.

context를 사용하면 그 아래의 전체 트리에 데이터가 제공되므로, 실제로 prop이 필요한 곳에서만 사용한다.

여기서 사용되는 예는 다음과 같다. 이를 context를 사용해 개선해보자.

  • Section 내부에 여러 개의 Heading이 존재한다.
  • 동일한 Section 내에는 동일한 Level의 Heading만 존재할 수 있다.
  • 기존 코드는 Heading에서 prop으로 Level을 입력받고 있다.

 

1. context를 만든다.

context용 파일을 생성하고, 컴포넌트에서 사용할 수 있도록 export 한다.

  • createContext의 인수로 디폴트 값(개체 포함 모든 타입 가능)을 설정할 수 있다.
// LevelContext.js

import { createContext } from 'react';

export const LevelContext = createContext(1);

2. data가 필요한 컴포넌트에서 context를 사용한다.

2-1. useContext Hook과 아까 생성한 context를 import 한다.

import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';

2-2. 기존 컴포넌트(여기서는 Heading)의 propscontext로 대체될 것은 제거하고, 제거된 prop를 가져오는 곳에는 context를 사용한다.

  • useContext HookHeading 컴포넌트가 LevelContext를 읽기를 원하다는 것을 React에 알리는 역할을 한다.
  • context를 제공하지않으면 React는 지정된 기본값을 사용한다.
// 기존 컴포넌트: level이 컨텍스트로 대체될 예정
export default function Heading({ level, children }) {
  // ...
}

// 컴포넌트가 context를 사용하도록 변경
export default function Heading({ children }) {
  const level = useContext(LevelContext);
  // ...
}

2-3. 기존 컴포넌트를 사용하는 곳에서 context로 대체되어 제거된 prop에 해당하는 부분을 제거한다.

2-4. 그 대신 제거된 propcontext를 제공할 컴포넌트에 전달한다.(여기서는 Section)

 

3. 데이터를 설정하는 컴포넌트에서 context를 제공한다.

context provider로 래핑 하여 LevelContext에게 값을 제공한다.

  • 이는 Section 내의 컴포넌트가 LevelContext를 요청하면 여기에 주어진 level을 제공하라고 React에게 말하는 것과 같다.
  • 단, 컴포넌트는 자신보다 위인 것 중 가장 가까운 <LevelContext.Provider>를 사용한다.
// 기존 Section
export default function Section({ children }) {
  return (
    <section className="section">
      {children}
    </section>
  );
}

// LevelContext 제공자를 래핑하도록 변경
import { LevelContext } from './LevelContext.js';

export default function Section({ level, children }) {
  return (
    <section className="section">
      <LevelContext.Provider value={level}>
        {children}
      </LevelContext.Provider>
    </section>
  );
}

 

결과

위의 3단계를 거치면 아래와 같이 코드가 완성된다.

각각의 Heading 컴포넌트 에 level prop을 사용하지 않는다. 대신 가장 가까운 Section에 아래와 같이 요청해서 파악한다.

  1. <Section>에서 level prop을 전달한다.
  2. Section에서 자식 요소를 <LevelContext.Provider value={level}>로 묶는다.
  3. HeadinguseContext(LevelContext)를 사용해 가장 가까운 LevelContext의 값을 요청한다.

 

 

동일한 컴포넌트에서 context 사용 및 제공

context를 사용하면 위의 컴포넌트를 통해 정보를 읽을 수 있기 때문에, 각각의 Section은 그 위의 Section으로부터 level을 읽고, 자동적으로 level + 1을 전달할 수 있다.

  • Heading, Section 에서는 LevelContext을 읽고 얼마나 깊이 있는지(level)를 파악한다.
  • Section LevelContextchildren를 래핑 하여 children가 "더 깊은" level(level + 1)에 있도록 지정한다.
// 기존 코드
export default function Page() {
  return (
    <Section level={1}>
      ...
      <Section level={2}>
        ...
        <Section level={3}>
          ...

// context를 사용
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';

export default function Section({ children }) {
  const level = useContext(LevelContext);
  return (
    <section className="section">
      <LevelContext.Provider value={level + 1}>
        {children}
      </LevelContext.Provider>
    </section>
  );
}

 

 

이 예제는 중첩된 컴포넌트가 context를 재정의할 수 있는 방법을 시각적으로 보여주기 위해 heading level을 사용한다.

 

context를 사용하면 "주변에 적응"해서 렌더링 되는 위치 (또는 컨텍스트)에 따라 다르게 표시되는 컴포넌트를 작성할 수 있다.

context를 제공하는 컴포넌트와 context를 사용하는 컴포넌트 사이에 얼마든지 컴포넌트를 삽입할 수 있다.

  • 삽입 가능 컴포넌트 : built-in 컴포넌트(`div`...) 또는 직접 만든 컴포넌트

따라서 동일한 컴포넌트라도 다른 중첩 레벨에 넣으면 자동적으로 가까운 context의 정보를 이용해 렌더링 된다.

아래 예제에서는 Post 컴포넌트가 어디에 위치하느냐에 따라 자동으로 <Heading> level이 적용된다.(App.js 참고)

 

 

다른 React context는 서로를 재정의하지 않는다. createContext()를 사용해 만들어지는 각 context는 다른 context와 완전히 분리되어 있으며 특정 context를  사용 및 제공하는 컴포넌트를 함께 묶는다. 따라서 하나의 컴포넌트는 문제없이 많은 다른 context를 사용하거나 제공할 수 있다.

 

context를 사용하기 전에

context를 사용하기 쉬운만큼 남용하기 쉽다. 일부 props를 여러 level의 깊이로 전달해야 한다고 해서 항상 이 정보를 컨텍스트에 넣어야 하는 것은 아니다.

context를 사용하기 전에 고려할 수 있는 대안이 몇 가지 있다.

  1. props를 전달하기.
    • 중요한 컴포넌트라면 수십 개의 컴포넌트를 통해 수십개의 prop을 전달하는 방법은 자주 사용된다.
    • 유지 보수 측면에서 props를 사용해 데이터 흐름(어떤 컴포넌트가 어떤 데이터를 사용하는지)을 명시적으로 만드는 것이 좋을 수 있다.
  2. 컴포넌트를 추출하고 JSX를 children으로 전달하기.
    • 데이터를 전달할 때 해당 데이터를 사용하지 않는 중간 컴포넌트들이 많을 경우(또 더 아래로만 전달할 경우), 이는 종종 도중에 일부 컴포넌트 추출을 제대로 하지 않았다는 의미일 수 있다.
      • 잘못된 예 : posts와 같은 data props을 직접 사용하지 않는 컴포넌트에 즉시 전달.
        • `<Layout posts={posts} />`
      • 올바른 예 : Layoutchildren을 porp으로 두고, 아래처럼 렌더링. => 이렇게 하면 데이터를 지정하는 컴포넌트와 이를 필요로 하는 컴포넌트 사이의 계층 수가 줄어든다.
        • `<Layout><Posts posts={posts} /></Layout>`

context를 위한 유즈 케이스

  • Theming: 앱에서 사용자가 모양을 변경할 수 있는 경우 (e.g. dark mode), context provider를 앱 상단에 배치하고, 시각적인 요소를 조정해야 하는 컴포넌트에서 해당 컨텍스트를 사용할 수 있다.
  • Current account: 많은 컴포넌트가 현재 로그인한 사용자를 알아야 할 수 있다. context 사용하면 트리의 어느 곳에서 나 쉽게 데이터를 읽을 수 있다. 일부 앱에서는 동시에 여러 계정을 운영할 수 도 있다. (예 : 다른 사용자로 댓글 남기기). 이때, UI 일부를 nested provider로 (현재 계정 값과) 다른 값을 래핑 하는 것이 편리하다.
  • Routing:  대부분의 라우팅 솔루션은 현재 경로를 유지하기 위해 내부적으로 context 를 사용한다. 이것이 모든 링크의 활성 여부를 "알 수 있는" 방법이다. 자신의 라우터를 구축하는 경우에도 그렇게 하고 싶을 수 있다.
  • Managing state: 앱이 성장함에 따라 앱 상단에 더 많은 상태가 생길 수 있다. 아래에 거리가 먼 많은 컴포넌트가 상태를 변경하려고 할 수 있다. 복잡한 상태를 관리하고 너무 많은 번거로움 없이 멀리 떨어진 구성 요소로 전달하기 위해 context와 함께 reducer를 사용하는 것이 일반적이다.

context의 값은 변경될 수 있다. 다음 렌더에서 다른 값을 전달하면, React는 아래에서 읽는 모든 구성 요소를 업데이트한다! 이것이 context가 종종 state와 함께 사용되는 이유이다.

일반적으로 트리의 다른 부분에 있는 멀리 떨어진 구성 요소에 일부 정보가 필요한 경우 컨텍스트가 도움이 될 것이라는 좋은 표시이다.

 

 

https://react.dev/learn/extracting-state-logic-into-a-reducer 를 읽고 정리한다.

리듀서 함수란

많은 이벤트 핸들러에 걸쳐 많은 상태 업데이트가 있는 컴포넌트는 다루기 힘들다.

이때 컴포넌트 외부에서 하나의 함수로 모든 상태 업데이트를 통합할 수 있다. 이를 "리듀서"라고 부른다.

상태 로직의 양이 늘어남에 따라 생기는 복잡성을 줄이고 모든 로직을 액세스 하기 쉬운 한 곳에 유지하기 위해 사용한다.

 

useState를 useReducer로 리팩토링하는 방법

1. 상태를 변경하는 함수 호출(setting state)에서 액션을 디스 패치하는 동작으로 변경해라

리듀서로 상태를 관리하는 것과 상태를 직접 설정하는 것은 다르다.

  • 상태를 설정하는 것은 React에 "할 일(what to do)"을 알려준다.
    • ex) tasks 이벤트 핸들러를 통해 상태를 설정
  • 리듀서로 상태를 관리하는 것은 이벤트 핸들러에서 액션을 디스패치함으로서 "사용자가 방금 한 일(what the user just did)"에 대해 알려준다.(상태 업데이트 로직은 다른 곳에 존재) 따라서 사용자의 의도를 더 잘 설명한다.
    • ex) tasks 이벤트 핸들러를 통해 작업 추가/변경/삭제 작업을 전달

dispatch로 전달하는 객체를 "action"이라고 한다.

  • js object이며 모든 형태의 데이터를 가질 수 있고, 발생한 일에 대한 최소한의 정보를 포함한다.
  • Convention : 보통 type을 사용해 발생한 일(deleted)을 설명하는 문자열을 제공하고 다른 필드에 추가 정보를 전달한다. 
function handleDeleteTask(taskId) {
  dispatch(
    // "action" object:
    {
      type: 'deleted',
      id: taskId,
    }
  );
}

2. reducer 함수를 작성해라

reducer 함수에 상태 로직을 넣는다. 현재 상태(state)와 작업 개체(action)의 두 인수를 사용하고 다음 상태를 반환하도록 한다.

React는 reducer에서 반환한 상태로 상태를 설정한다.

function yourReducer(state, action) {
  // return next state for React to set
}
reducer 함수는 상태를 인수로 취하기 때문에 컴포넌트 외부에서 선언할 수 있다.
이렇게 하면 들여쓰기가 줄어들고 코드를 더 쉽게 읽을 수 있다.
Note

reducer 내부에서 보통 if/else문보다 switch문을 사용한다. 결과는 같지만 switch 문을 한눈에 읽는 것이 더 쉽다.
또한 서로 다른 내부에서 선언된 변수가 서로 충돌하지 않도록 각 case 블록을 중괄호로 묶는 것이 좋다.
case문은 return문으로 끝난다. 그렇지 않으면 코드가 다음 case로 "떨어질 수 있습니다(fall through)".
Deep dive : React의 reducer는 배열의 reduce()연산과 비슷한 형식을 가지고 있다.

reducer는 컴포넌트 내부의 코드 양을 줄일 수(reduce)도 있지만, 배열에서 사용하는 reduce()연산을 본떠서 이름이 지어졌다.
reduce는 배열을 선택하고 많은 값 중에서 단일 값을 "누적"할 수 있다.
여기서 reduce로 전달하는 함수를 reducer라고 한다.

React의 reducer는 같은 아이디어를 사용한다. 따라서 reducer는 지금까지의 상태와 액션을 취하고 다음 상태를 반환하도록 동작한다. 이런 식으로 시간이 지남에 따라 행동을 상태로 축적한다.

 

3. 컴포넌트에서 reducer를 사용해라

react의 useReducer Hook을 사용한다.

import {useReducer} from 'react';

그런 다음 useStateuseReducer로 변경한다.

// useState
const [tasks, setTasks] = useState(initialTasks);
// useReducer
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

useReducer Hook과 useState는 초기 상태 전달/상태 값을 반환/상태를 설정(dispatch 함수)과 유사하다.

하지만 조금 다른 점이 있다.(useReducer Hook 기준)

  • 두가지 인수를 취한다.
    • reducer function
    • initial state
  • 반환 값
    • 다음 상태 값
    • 디스패치 기능(사용자 작업을 reducer에게 디스 패치하기 위해)

 

결론적으로 이벤트 핸들러는 액션을 디스 패치하여 발생한 일만 지정하고 리듀서 함수는 이에 대한 응답으로 상태가 업데이트되는 방식을 결정한다.

 

useStateuseReducer 비교

컴포넌트에서 잘못된 상태 업데이트로 인해 버그가 자주 발생하고 해당 코드에 더 많은 구조를 도입하려는 경우 reducer를 사용하는 것이 좋다. 하지만 모든 것에 reducer를 사용할 필요는 없다. 자유롭게 장단점에 맞게 사용하면 된다.

  • code size
    • useState : 일반적으로 코드가 작다.
    • useReducer : 리듀서 함수와 디스패치 작업을 해야 하기 때문에 코드량이 많다. 하지만 많은 이벤트 핸들러가 유사한 방식으로 상태를 수정하는 경우 코드를 줄이는 데 도움이 될 수 있다.
  • 가독성
    • useState : 상태 업데이트가 단순할 때 읽기 쉽다.
    • useReducer : 상태 업데이트가 복잡해서 컴포넌트를 읽기 어려울 경우 사용한다. 그러면 이벤트 처리기에서 발생한 것과 상태 업데이트 로직이 분리되어 명확히 구분할 수 있다.
  • 디버깅
    • useState : 버그가 있는 경우, 상태가 잘못 설정된 위치와 이유를 구별하기 어렵다.
    • useReducer : console log를 리듀서에 추가해 모든 상태 업데이트와 발생한 이유를 확인할 수 있다. action 호출과 reducer 로직을 나누어서 파악할 수 있다. 하지만 useState를 사용하는 것보다 더 많은 코드를 단계별로 실행해야 한다.
  • 테스팅
    • useState : 컴포넌트에 의존되어있다.
    • useReducer : 리듀서는 순수 함수기 때문에 별도로 내보내고 테스트할 수 있다. 현실적인 환경에서 컴포넌트를 테스트하는 것이 가장 좋지만 복잡한 상태 업데이트 로직의 경우 reducer가 특정 초기 상태 및 작업에 대해 특정 상태를 반환한다고 assert하는 것이 유용할 수 있다.
  • 개인 취향
    • 개인 취향은 모두 다를 수 있다. 여기서는 useState <-> useReducer 는 상호 호환 가능하다.

reducer를 잘 사용하는 팁

Reducer는 순수해야 한다.

  • 상태 업데이터 함수와 비슷하게 reducer는 렌더링 중에 실행된다. 이는 reducer가 순수해야 함을 의미한다.
  • 동일한 입력은 항상 동일한 출력을 낳아야 한다. 요청을 보내거나 시간 초과를 예약하거나 side effect를 수행하면 안 된다.
  • 객체배열을 주의해서 업데이트해야 한다. 이는 immer를 사용하면 간결하게 할 수 있다.

각 action은 single user의 상호작용을 묘사한다. 이는 데이터가 여러 번 변경되는 경우도 마찬가지다.

  • 예를 들어 사용자가 reducer에서 관리하는 5개의 필드가 있는 양식에서 "재설정"을 누르면 5개의 개별 설정 작업보다 하나의 작업을 전달해야 한다.
  • 작업 기록 시 로그가 상호작용이나 응답이 어떤 순서로 발생했는지 재구성할 수 있을 만큼 명확해야 디버깅 시 도움이 된다.

Immer로 간결한 reducer 작성하기

Reducer는 순수해야 하므로 상태를 변경하면 안 된다.

Immer에서는 draft 객체를 사용해 변경을 안전하게 할 수 있다.

  • 예를 들어 useImmerReducer를 사용해 배열이나 객체를 업데이트할 때 간결하게 push, arr[i]= 할당 문을 사용해 상태를 변경할 수 있다.

 

challenges

4번 문제 : useReducer를 간단히 구현하라.

useReducer는 action을 dispatch하면 현재 state와 action이 있는 reducer를 호출하고 그 결과를 다음 state로 저장한다.

import {useState} from 'react';

export function useReducer(reducer, initialState) {
  const [state, setState] = useState(initialState);

  function dispatch(action) {
    const nextState = reducer(state, action);
    setState(nextState);
  }

  return [state, dispatch];
}

더 정확한 구현은 다음과 같다.

function dispatch(action) {
  setState((s) => reducer(s, action));
}

https://react.dev/learn/sharing-state-between-components 를 읽고 정리한다.

 

두 컴포넌트의 state가 항상 함께 변경되게 하고 싶을 때는 가장 가까운 공통 부모로 이동한 다음 props를 통해 전달한다.

이 것은 상태 들어올리기(lifting state up)이라고 불린다.

3단계로 상태 들어올리기를 수행할 수 있다.

1. 자식 컴포넌트에서 state를 제거한다.

2. 공통 부모로부터 하드코딩된 데이터를 전달한다.

3. 공통 부모에 상태를 추가하고 (자식에게) 이벤트 핸들러와 함께 전달한다.

  • 상태를 올릴때 상태로 저장하는 항목의 특성이 달라질 수 있다.
    • 예시) 기존에는 `panel`에서 활성화 여부를 나타내는 `isActive`를 사용했지만, 끌어올려진 후 그 부모 컴포넌트에서는 어떤 `panel`이 활성화되었는 지를 저장하는 것으로 변경되었다.
  • 부모에서 이벤트 핸들러를 자식에게 전달하면 자식이 부모의 상태를 변경할 수 있다.
Deep Dive : controlled / uncontrolled 컴포넌트

uncontrolled 컴포넌트는 자신의 local state를 직접 통제하는 것을 말한다.
- uncontrolled 컴포넌트는 추가적인 구성이 덜 필요하기 때문에 부모에서 사용하기가 더 쉽다.
- 다른 것과 함께 조정하려는 경우 유연성이 떨어진다.
controlled 컴포넌트는 중요한 정보가 자체 local state가 아닌 props에 의해 구동(제어)되는 경우를 말한다.
- controlled 컴포넌트는 많이 유연하지만, props로 컴포넌트를 완전히 구성하려면 부모 컴포넌트가 필요하다.

컴포넌트를 작성할 때 어떤 정보가 제어되어야 하는지(props를 통해) 어떤 정보가 제어되지 않아야 하는지(상태를 ​​통해) 고려하십시오.
그러나 언제든지 마음을 바꾸고 나중에 리팩토링할 수 있습니다.

각각의 고유한 state는 어떤 컴포넌트가 소유할 것인지 골라야한다. 이 원칙은 "single source of truth"라고 알려져있다.

  • 모든 state가 한 장소에 있다는 것이 아닌 각 state는 이를 보유하는 하나의 컴포넌트가 있다는 것을 의미한다.
  • 따라서 컴포넌트 간의 공유 state를 복제하는 것이 아닌 공통 공유 부모 컴포넌트로 lift up 해서 필요한 자식에게 전달한다.

https://react.dev/learn/choosing-the-state-structure 를 읽고 정리한다.

 

state를 잘 구성하면 수정 및 디버깅하기 용이한 컴포넌트를 만들 수 있다.

state 구성 원칙

이 원칙은 실수없이 상태를 업데이트하는 것이 목표이다. 이는 데이터베이스 구조를 정규화하여 버그 가능성을 줄이는 방법과 유사하다.

즉, 중복을 줄이면 모든 부분이 동기화 상태를 유지하는 데 도움이 된다.

 

1. 연관된 상태를 묶어라

항상 두 개 이상의 상태 변수를 동시에 업데이트 하는 경우, 단일 상태 변수로 병합해라.

 

state를 object 또는 array로 사용하는 경우는 얼마나 많은 상태가 필요한지 모르는 경우에 사용한다. 예를 들어 사용자가 사용자 정의 필드를 추가할 수 있는 양식이 있는 경우, 사용자가 얼만큼 추가할지 예상할 수 없으므로 이때 유용하게 사용된다.

state를 object로 만들 경우, 한 field만 업데이트할 수 없다.
이를 해결할 수 있는 방법은 아래와 같다.
1. `...`을 사용해 기존 상태를 펼친 후, 업데이트할 field만 재정의하는 방식으로 사용할 수는 있다. setObject({...object, x: 100 })
2. object로 된 변수를 두 개로 분리한 후 따로 업데이트 할 수 있도록 한다. setX(100)

2. 서로 반대되는 상태를 만들지 마라

서로 반대되는 상태를 만들면 실수가 생길 수 있다.

 

예를 들어, isSending, isSent 상태 변수를 사용한 호텔 피드백 양식이 있다고 하자.

이 코드는 isSending, isSent가 동시에 같은 값을 가질 수 없다. 또한 두 상태를 같이 세팅하는 것을 잊어버리면 버그가 발생하기 쉽다.

 

따라서 isSending, isSent가 동시에 같은 값을 가질 수 없으므로, 3가지 유효한 상태를 묶은 하나의 상태 변수를 사용하는 것이 좋다.

  • typing(inital), sending, sent

3. 불필요한 상태를 만들지 마라

렌더링시 컴포넌트의 prop 또는 state 변수에서 계산할 수 있는 정보는 state로 만들지 마라.

 

Deep Dive : state에 props를 미러링하면 안된다.

여기서는 color state 변수가 messasgeColor prop으로 초기화된다.
function Message({ messageColor }) {
  const [color, setColor] = useState(messageColor);​

 

state는 첫번째 렌더링 중에만 초기화된다. 따라서 중간에 상위 컴포넌트에서 다른 값을 전달하면 이를 미러링한 state값이 업데이트되지 않는다.

이때문에 state 변수를 prop으로 미러링하면 혼동을 일으킬 수 있다.
대신 messasgeColor prop은 코드에서 직접 사용해야한다.
function Message({ messageColor }) {
  const color = messageColor;​

 

만약, 특정 prop에 대한 모든 업데이트를 무시하고 싶으면 props를 state로 미러링해도 된다. 이 때, 규칙에 따라 prop 의 이름을 initial, default를 사용해 업데이트가 무시된다는 것을 표현해라.

4. 중복된 상태를 만들지 마라

여러 state 변수 사이나 중첩된 개체 내에서 동일한 데이터가 중복되는 경우 동기화를 유지하기가 어렵다. 가능하면 중복을 줄이자.

특히 선택같은 경우, 개체 자체가 아닌 ID 또는 인덱스로 상태를 유지해라.

5. 깊게 중첩된 상태를 만들지 마라

중첩된 상태는 변경된 부분외에도 중첩되어있는 모든 데이터를 업데이트 해야하기 때문에 어렵다. 가능하면 flat한 방법으로 상태를 구성하라.

flat하게 만들면 상태를 더 쉽게 업데이트할 수 있으며 중첩된 개체의 다른 부분에 중복이 없는지 확인하는 데 도움이 된다.

 

예를 들어, 아래와 같은 중첩된 객체가 있다고 하자.

여기서 하나의 장소를 제거하는 동작을 추가하려면 동작이 실행될 때마다 전체 상위 장소 체인을 복사해야한다.

 

이를 평평하게 만드려면 각 장소마다 ID와 정보를 따로 가지고, 추가로 하위 장소 ID의 배열을 사용해 매핑을 하면 된다.(마치 데이터베이스 테이블 처럼)

 

이렇게 되면 중첩 항목 업데이트가 더 쉬워집니다.

이 상태에서 하나의 장소를 제거하는 동작을 추가하려면 두 가지 상태만 업데이트하면 된다.

  • 상위 장소에서 childIds에 제거하려는 id를 제거
  • 루트에서 상위 장소의 업데이트된 버전이 포함되어야한다.

따라서 아래와 같이 수행할 수 있다.

 

 

 

https://react.dev/learn/reacting-to-input-with-state를 보고 정리한다.

 

React는 선언적 방법을 사용하여 UI를 조작한다. 이는 디자이너가 UI에 대해 생각하는 방식과 유사하다.

  • UI를 직접 조작하지 않는다.
  • 컴포넌트는 존재할 수 있는 다양한 상태를 가진다. 이를 이용해 사용자 입력에 대한 응답으로 상태를 바꾼다.

 

선언적(declarative) UI vs 명령형(imperative) UI

명령형 프로그래밍은 현재 발생한 상황에 따라 UI를 조작하기 위해 정확한 가이드라인을 작성해야 한다.

만약 답변을 제출하는 양식을 만들 때 명령적으로 구성하면 다음과 같이 생각할 수 있다.

  • 양식에 무언가를 입력하면 "제출" 버튼 이 활성화됩니다.
  • "제출"을 누르면 양식과 버튼 이 모두 비활성화되고 스피너 가 나타납니다.
  • 네트워크 요청이 성공하면 양식 이 숨겨지고 "감사합니다"라는 메시지 가 나타납니다.
  • 네트워크 요청이 실패하면 오류 메시지 가 나타나고 양식 이 다시 활성화됩니다.

즉, 명령형 프로그래밍은 구성 요소마다 명령(활성화/비활성화 등..)하여 컴퓨터에 UI를 업데이트하는 방법 알려줘야 한다.

 

UI를 직접 조작하는 것은 복잡한 시스템에서는 관리하기가 기하급수적으로 어려워진다. 이 문제를 해결하기 위해 React가 만들어졌다.

  • 만약 새로운 UI 요소나 상호작용을 추가하려면 버그가 발생하지 않는지 모든 기존 코드를 주의 깊게 확인해야 한다.

 

React는 선언적 프로그래밍을 사용한다. 즉, 무엇을 보여줄지 선언하고, UI를 업데이트하는 방법은 React가 알아내서 처리한다.

UI를 선언적으로 생각하기

1단계 : 컴포넌트의 다양한 시각적인 state를 식별한다.

React는 두 아이디어(컴퓨터 과학, design)를 통해 영감을 얻었다.

  • 컴퓨터 과학 : state machine 개념
  • design : 다양한 "visual states"에 대한 목업 개념

따라서 사용자가 볼 수 있는 UI의 다양한 "state"를 식별해야 한다.

답변을 제출하는 양식에서는 아래와 같은 state를 식별할 수 있다.

  • 비어 있음 : 양식에 비활성화된 "제출" 버튼이 있습니다.
  • 입력 : 양식에 "제출" 버튼이 활성화되어 있습니다.
  • 제출 중 : 양식이 완전히 비활성화되었습니다. 스피너가 표시됩니다.
  • 성공 : 폼 대신 "감사합니다" 메시지가 출력됩니다.
  • 오류 : 입력 상태와 동일하지만 추가 오류 메시지가 있습니다.

logic을 추가하기 전에 다양한 상태에 대해 mocking을 만들어 확인할 수도 있다.

아래 예제에서는 state 값(empty/success)을 변경해서 올바르게 표시되는지 확인할 수 있다.

 

 

한 번에 많은 시각적인 state 표시하기

많은 state가 존재하는 경우, 한 페이지에 모두 표시하는 것이 편할 수 있다.

 

2단계 : 무엇이 state를 변경시키는지 파악한다.

state 업데이트를 트리거할 수 있는 것은 두 가지가 존재한다. 두 가지 모두 UI를 업데이트하기 위해 state 변수를 변경해야 한다.

  • Human inputs : 버튼 클릭, 필드 입력, 링크 탐색...
  • Computer inputs : 네트워크 응답 도착, 시간 초과, 이미지 로드...

<예>

아래와 같은 입력이 있고, 입력에 대한 응답으로 state를 변경할 수 있다.

  • 텍스트 입력(human) : text box(empty or not)에 따라 Empy or Typing state로 전환
  • 제출 버튼(human) : 클릭시 Submitting state로 전환
  • 성공적인 네트워크 응답(컴퓨터)  : Success state로 전환
  • 실패한 네트워크 응답(컴퓨터) : 에러 메시지와 함께 Error state로 전환

state(원)와 이를 변화시키는 입력(화살표)을 시각화하면 아래와 같다. 구현하기 전에 flow를 스케치하면 혹시 모를 버그를 찾을 수 있다.

3단계 : useState를 사용해 메모리에 state를 표현한다.

키포인트는 단순함이다. : 가능한 가장 적은 state로 구성되도록 해야 한다. 복잡할수록 더 많은 버그가 발생한다!

따라서 반드시 있어야 하는 state부터 시작해야 한다.

<예> - 총 7개 생성

  1. input을 위한 answer와 마지막 오류를 저장하기 위한 error가 필요하다.
  2. 1단계에서 식별한 시각적인 state를 표시한다. 이를 표현하는 방법은 여러 가지인데, 가장 좋은 방법이 생각나지 않는다면 모든 상태를 추가하는 것으로 시작하고 그 후에 리팩터링 하도록 한다.
    • isEmpty, isTyping, isSubmitting, isSuccess, isError

4단계 : 불필요한 state 변수를 제거한다.

state의 내용이 중복되지 않게 필수적인 것만 추적해야 한다.

여기서 목표는 저장한 state가 사용자에게 제대로 된 UI를 표현하지 못하는 경우를 막는 것이다.

불필요한 state 변수를 제거하기 위해 아래의 질문들에 대해 생각해보자

  • state가 역설을 일으키는가(모순이 존재하는 가)?
    • isTyping, isSubmitting은 같이 true가 될 수 없다. 그래서 2가지 boolean은 4가지 조합이 나오지만, 여기서는 3가지만 가능하다.
    • 불가능한 state(위에 설명)를 지우기 위해 하나의 state에 아래 3가지 state 중 하나만 설정되도록 할 수 있다.
      • 'typing', 'submitting', 'success'.
  • state 변수 간에 중복 정보가 존재하는가?
    • isTyping, isEmpty은 같이 true가 될 수 없다. 이들을 별도의 state 변수로 만들면 동기화되지 않고 버그가 발생할 위험이 있다. 
    • isEmpty를 제거하고 answer.length === 0을 체크하도록 대신할 수 있다.
  • 다른 state 변수를 뒤집으면 동일한 정보를 얻을 수 있는가?
    • isErrorerror != null로 대신할 수 있다.

위의 내용을 정리하면 아래처럼 7 -> 3개로 정리된다.

const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);
const [status, setStatus] = useState('typing'); // 'typing', 'submitting', or 'success'

5단계 : state를 설정하기 위해 이벤트 핸들러와 연결한다.

마지막으로 이벤트 핸들러를 생성하여 상태 변수를 설정한다. 다음은 모든 이벤트 핸들러가 연결된 최종 형식이다.

 

이 코드는 원래 명령형 예제보다 길지만 훨씬 덜 취약하다. 모든 상호 작용을 상태 변경으로 표현하면 나중에 기존 상태를 손상시키지 않고 새로운 시각적 상태를 도입할 수 있다. 또한 상호 작용 자체의 로직을 변경하지 않고도 각 상태에 표시되어야 하는 내용을 변경할 수 있다.

 

Challeges

몰랐던 개념이나 다르게 푼 경우, 기록을 남긴다.

1. 이벤트 버블링 막기 : e.stopPropagation() 사용

2. form submit 시 기본 동작 막기(전체 페이지를 다시 로드) : e.preventDefault()

 

+ Recent posts