Describing the UI

Your First Component

Importing and Exporting Components

Writing Markup with JSX

Javascript in JSX with Curly Braces

Passing Props to a Component

2023.03.05 - [React/Learn React] - Describing the UI ☞ Conditional Rendering

Rendering Lists

2022.09.24 - [React/Learn React] - Describing the UI ☞ Keeping Components Pure

Adding Interactivity

Responding to Events

State: A Component's Memory

2022.09.24 - [React/Learn React] - Adding Interactivity ☞ Render and Commit

2022.09.25 - [React/Learn React] - Adding Interactivity ☞ State as a Snapshot

2022.12.04 - [React/Learn React] - Adding Interactivity ☞ Queueing a Series of State Updates

Updating Objects in State

Updating Arrays in State

Managing State

2022.09.25 - [React/Learn React] - Managing State ☞ Reacting to Input with State

2022.10.01 - [React/Learn React] - Managing State ☞ Choosing the State Structure

2022.10.01 - [React/Learn React] - Managing State ☞ Sharing State Between Components

2023.04.01 - [React/Learn React] - Managing State ☞ Preserving and Resetting State

2022.10.03 - [React/Learn React] - Managing State ☞ Extracting State Logic into a Reducer

2022.10.30 - [React/Learn React] - Managing State ☞ Passing Data Deeply with Context

2022.10.31 - [React/Learn React] - Managing State ☞ Scaling Up with Reducer and Context

Escape Hatches

2022.12.04 - [React/Learn React] - Escape Hatches ☞ Referencing Values with Refs

2023.04.23 - [React/Learn React] - Escape Hatches ☞ Manipulating the DOM with Refs

Synchronizing with Effects

You Might Not Need an Effect

Lifecycle of Reactive Effects

Separating Events from Effeects

Removing Effect Dependencies

Reusing Logic with Custom Hooks

https://react.dev/learn/manipulating-the-dom-with-refs 를 읽고 정리한다.

 

리액트는 렌더링 된 결과물을 매칭하기 위해 DOM을 자동적으로 업데이트한다. 그래서 대부분의 컴포넌트는 따로 조작할 필요가 없다. 그러나 가끔, 리액트가 관리하는 DOM 요소를 접근하는 경우도 있다.

  • 예를 들어 node에 focus, scroll, 또는 node의 사이즈나 위치를 측정하는 일이 있다.

리액트에서는 이런 것들을 할 수 있는 내장된 방법이 없다, 그래서 DOM node에 ref가 필요하다.

 

node의 ref 얻기

1. useRef 훅을 import한다.

import { useRef } from 'react';

2. 컴포넌트 내부에서 useRef 훅을 사용해 ref를 정의한다.

const myRef = useRef(null);

3. DOM node에 ref 어트리뷰트로 전달한다.

<div ref={myRef}>

 

useRef 훅은 current로 불리는 단일 속성을 가지는 객체를 반환한다.

  • 초기에 myRef.currentnull이다.
  • 그 후 리액트가 DOM node를 생성할 때, 리액트는 node의 참조값을 myRef.current에 넣는다. 그런 다음 DOM node에 접근하여 이벤트 핸들러나 내재된 browser API를 사용할 수 있다.
// You can use any browser APIs, for example:
myRef.current.scrollIntoView();

 

예: text input에 focus 주기

버튼을 누르면 text input에 focus를 주도록 구현해 보자.

 

 

1. useRef 훅을 사용해 inputRef 를 정의한다.

2. <input ref={inputRef}>로 전달한다. 이는 리액트에게 inputRef.current<input>의 DOM 노드를 전달하라는 의미이다.

3. handleClick 함수에서, inputRef.current를 사용해 input DOM을 읽고 inputReft.current.focus()focus()를 호출한다.

4. handleClick 이벤트 핸들러를 <button>onClick으로 전달한다.

 

DOM 조작은 ref를 사용하는 가장 흔한 경우이지만, timer ID와 같은 리액트 밖의 것을 저장하기 위해 사용되기도 한다. state과 유사하게 ref는 렌더링사이에 유지된다. Ref는 state 변수와 비슷하지만 설정 시 리렌더링을 트리거하지 않는다. 이는 Referencing Values with Refs에서 확인할 수 있다.

예: 요소에 Scroll 하기

컴포넌트에서 ref는 여러 개가 있을 수 있다. 예에서는 3개 이미지가 있다. 각 버튼은 DOM node에서 브라우저의 scrollIntoView()를 호출하여 이미지를 중앙에 배치한다.

 

DEEP DIVE: ref 콜백을 사용해 ref 목록을 관리하기

위의 예를 보면 refs의 수가 사전 정의되어 있다. 그러나, 가끔 목록의 각 아이템에 ref가 필요한데 아이템의 총개수를 모를 수 있다. 이때 아래의 코드는 동작하지 않는다.

<ul>
  {items.map((item) => {
    // Doesn't work!
    const ref = useRef(null);
    return <li ref={ref} />;
  })}
</ul>

그 이유로 훅은 컴포넌트의 최상위 레벨에서만 호출할 수 있기 때문이다. 그래서 루프 내부나 조건 또는 map() 호출 내부에서 useRef를 호출할 수 없다.

 

1. 한 가지 가능한 방법은 부모 요소의 ref를 얻고, querySelectorAll과 같은 DOM 조작 메서드를 사용하여 개별 자식 노드를 "찾는" 것이다. 그러나 이 방법은 깨지기 쉬우며, DOM 구조가 변경되면 중단될 수 있다.

 

2. 다른 해결책으로는 ref 어트리뷰트에 함수를 전달하는 것이다. 이는 ref callback으로 불린다. 리액트는 ref가 설정되면 DOM node의 ref callback을 호출하고, 지울 때가 되면 null로 호출한다. 이를 통해 자신의 배열이나 맵(Map)을 유지 관리하고 index나 ID로 모든 참조에 접근할 수 있다.

이 예제는 이 접근 방법을 통해 긴 목록에서 임의의 노드를 스크롤하는 방법을 보여준다.

 

 

이 예에서 itemsRef는 단일 DOM node를 가지지 않는다. 대신 item ID에서 DOM node로 접근할 수 있는 Map을 가진다.(Ref는 모든 값을 가질 수 있다!) 모든 list item의 ref callback은 Map을 업데이트한다.

<li
  key={cat.id}
  ref={node => {
    const map = getMap();
    if (node) {
      // Add to the Map
      map.set(cat.id, node);
    } else {
      // Remove from the Map
      map.delete(cat.id);
    }
  }}
>

이렇게 하면 나중에 Map에서 개별적인 DOM node를 읽을 수 있다.

 

다른 컴포넌트의 DOM node로 접근하기

내장 컴포넌트(<input />)에 ref를 넣을 경우, 리액트는 해당 ref의 current 프로퍼티를 해당 DOM 노드(브라우저 내 실제 <input />)로 설정한다.

 

그러나 <MyInput />과 같이 자신의 컴포넌트에 ref를 넣고 싶은 경우, 기본적으로 null을 얻게 된다. 다음은 이를 보여주는 예시이다. 버튼을 클릭해도 input에 focus가 되지 않는 것을 확인할 수 있다.

 

 

이슈 확인을 돕기 위해, 리액트는 콘솔에 아래와 같은 에러를 출력한다.

Warning: Function components cannot be given refs.
Attempts to access this ref will fail. Did you mean to use React.forwardRef()?

리액트가 기본적으로 다른 컴포넌트의 DOM node에 접근하는 것을 허용하지 않기 때문에 발생한다. 심지어 자식도 마찬가지이다! 이는 의도적이다.

ref는 탈출구이기 때문에 아주 조금만 사용해야 한다. 다른 컴포넌트의 DOM node를 수동으로 조작하면 코드가 훨씬 더 깨지기 쉬워진다.(취약해진다.)

 

만약 자신의 DOM 노드를 내보내길(export) 원하는 컴포넌트들은 forwardRef API를 사용해 구현할 수 있다.

  • 이때, 컴포넌트는 자신의 ref를 자식 중 하나만 "전달" 가능하다. 따라서 내보낼 자식을 선택해야 한다.

MyInput forwardRef API를 사용하는 방법은 다음과 같다.

const MyInput = forwardRef((props, ref) => {
  return <input {...props} ref={ref} />;
});

이는 아래와 같이 동작한다.

1. <MyInput ref={inputRef} />는 리액트가 inputRef.current에 해당 DOM node에 넣으라고 지시한다. 그러나 이를 선택할지는 MyInput 컴포넌트의 선택에 달려있으며, 기본적으로 그렇지 않다.

2. MyInput 컴포넌트는 forwardRef를 사용해 정의되어 있다. 이는 props 다음에 선언되는 두 번째 ref인수로 inputRef를 받도록 선택된다.

3. MyInput은 받은 ref를 그 안에 있는 <input>으로 전달한다.

 

이제 버튼을 클릭하면 input에 focus가 동작한다.

 

 

디자인 시스템에서 button, input 등과 같은 낮은 수준의 컴포넌트는 해당 ref를 DOM node로 전달하는 것은 흔한 패턴이다. 반면에, 고수준의 컴포넌트는 DOM 구조에 대한 우발적인 종속성을 피하기 위해 일반적으로 해당 DOM node를 노출(export) 하지 않는다.

 

DEEP DIVE : imperative handle를 사용해 API의 하위 집합 노출하기

위 예에서 MyInput은 원본 DOM input 요소를 노출한다. 이는 부모 컴포넌트가 focus()를 호출할 수 있도록 허락한다. 이는 또한 부모 컴포넌트가 다른 것들도 할 수 있다. 예를 들어, CSS style도 변경할 수 있다. 드문 경우로, 노출되는 기능을 제한하고 싶을 수도 있다. useImperativeHandle로 이를 수행할 수 있다.

 

 

여기 MyInputrealInputRef은 실제 input DOM node를 가진다. 그러나, useImperativeHandle은 리액트가 부모 컴포넌트에게 ref 값으로 고유한 특수 객체를 공급하도록 지시한다. 그래서 Form 컴포넌트 내에 inputRef.cureent는 오직 focus 메서드만 가질 수 있다. 이 경우에, ref "handle"은 DOM node가 아니라 useImperativeHandle 호출 내에서 생성한 사용자 지정 객체이다.

 

리액트가 ref 설정하는 시점 : 커밋 중 DOM 업데이트가 되었을 때

리액트에서 모든 업데이트는 두 단계로 나뉜다.

  • 렌더링 하는 동안, 리액트는 컴포넌트를 호출하여 화면에 무엇이 표시되어야 하는지 파악한다.
  • 커밋하는 동안, 리액트는 DOM에 변경사항을 적용한다.

보통 렌더링동안 ref에 접근하는 것을 원하진 않는다. 그것은 DOM node를 가진 ref에 대해서도 마찬가지이다.

  • 첫 번째 렌더링 동안, DOM node는 아직 생성되지 않았고, 그래서 ref.cureent는 null이 될 것이다.
  • 그리고 변경사항이 렌더링 될 동안, DOM node는 아직 변경사항이 적용되지 않았다. 따라서 ref를 읽기에는 너무 이르다.

리액트는 커밋하는 동안 ref.current를 설정한다.

  • DOM이 업데이트되기 전, 리액트는 영향을 받는 ref.current 값을 null로 설정한다.
  • DOM이 업데이트된 후, 리액트는 즉시 연관된 DOM node를 ref에 설정한다.

보통 이벤트 핸들러를 통해 ref에 접근한다. 만약 ref로 무언가를 하고 싶지만 이를 수행할 특정 이벤트가 없는 경우, Effect가 필요할 수 있다. Effect에 대해서는 다음 페이지에서 설명하겠다.

 

DEEP DIVE : flushing state는 flushSync와 동기적으로 업데이트된다.

새 할 일을 추가하고, 목록의 마지막 자식까지 화면을 아래로 스크롤하는 코드를 고려해 보자. 항상 마지막으로 추가한 항목 바로 앞에 있던 할 일로 스크롤되는 문제를 확인할 수 있다.

 

 

이 문제는 아래 두 줄과 연관된다.

setTodos([ ...todos, newTodo]);
listRef.current.lastChild.scrollIntoView();

리액트에서 state 업데이트는 큐로 수행한다. 일반적으로 이는 사용자가 원하는 것이다. 그러나, 여기에서는 setTodos가 DOM에 즉시 업데이트가 되지 않기 때문에 발생한 문제이다. 그래서 마지막 요소로 목록을 스크롤할 때는 아직 할 일이 추가되지 않은 상태이다. 이것이 스크롤이 항상 한 항목씩 "뒤처지는" 이유이다.

 

이 문제를 해결하기 위해, 리액트에게 DOM을 동기적으로 업데이트("flush")하게 강제할 수 있다. 이를 수행하기 위해, react-dom을 통해 flushSync를 임포트 하고 state 업데이트를 flushSync 호출로 감싼다.

flushSync(() => {
  setTodos([ ...todos, newTodo]);
});
listRef.current.lastChild.scrollIntoView();

이는 flushSync로 감싸진 코드가 실행된 직후 리액트가 DOM을 동기적으로 업데이트하도록 지시한다. 그 결과, 스크롤을 시도할 때 마지막 할 일은 이미 DOM에 존재한다.

 

ref를 이용한 DOM 조작 모범 사례

ref는 탈출구이다. "리액트 밖으로 나가야 할 때"에만 이를 사용해야 한다.

 

일반적인 예로는 focus, scroll 위치를 관리하거나 리액트가 노출하지 않는 브라우저 API를 호출하는 것이 있다. focus, scroll과 같은 비파괴적인 동작을 붙이는 경우에는 문제가 발생하지 않는다.

하지만 DOM을 수동적으로 수정하려고 하면, React가 수행하는 변경 사항과 충돌할 위험이 있다.

 

이 문제를 설명하기 위해, 이 예시에는 환영 메시지와 두 개의 버튼이 있다. 첫 번째 버튼은 조건부 렌더링state를 사용해 그 존재여부를 전환한다. 두 번째 버튼은 remove() DOM API를 사용해 리액트가 제어할 수 없는 DOM에서 강제로 제거한다.

 

"Toggle with setState"을 여러 번 누르면 메시지는 사라지고 나타나기를 반복할 것이다. 그리고 "Remove from the DOM"을 누르면 강제로 메시지가 지워진다. 마지막으로 "Toggle with setState" 눌러보자

 

 

DOM 요소를 수동으로 지운 후, 다시 보여주기 위해 setState를 사용하면 충돌이 난다. 이는 사용자가 DOM을 변경했고, 리액트가 이를 계속 올바르게 관리하는 방법을 모르기 때문이다.

 

리액트가 관리하는 DOM node를 변경하는 것을 피해라. 리액트가 관리하는 요소를 수정하거나 자식을 추가/삭제하는 것은 일관성 없는 시각적 결과 또는 위와 같은 충돌이 발생할 수 있다.

 

그러나, 이를 전혀 할 수 없다는 이야기가 아니라 주의가 필요하다는 이야기이다. 리액트가 업데이트할 이유가 없는 DOM의 일부를 안전하게 수정할 수 있다. 예를 들어, JSX에 일부 <div>가 항상 비어있다면 React는 그 자식들을 건드릴 이유가 없다. 따라서 수동적으로 요소를 추가/제거하는 것이 안전하다.

 

https://react.dev/learn/preserving-and-resetting-state를 읽고 정리한다.

state는 컴포넌트 사이에 격리된다.

리액트는 UI 트리에서 어떤 컴포넌트가 어떤 state에 속해 있는지를 추적한다.

개발자는 리렌더링 사이에 state 보존(preserve) 또는 초기화(reset) 시기를 제어할 수 있다.

UI tree

브라우저들은 UI를 모델링하기위해 많은 트리 구조를 사용한다.

리액트 또한 UI를 관리하고 모델링하기위해 트리 구조를 사용한다.

  1. React는 JSX로부터 UI 트리를 만든다.
  2. 그런 다음 React DOM은 해당 UI와 일치하도록 브라우저 DOM 요소들을 업데이트한다.

컴포넌트로 리액트는 UI 트리를 생성하며, DOM을 렌더링하기 위해 React DOM이 UI 트리를 사용한다.

state는 트리 내 위치와 엮여있다.

컴포넌트 state가 주어지면, 컴포넌트 내부에 state가 "존재"한다고 생각할 수 있다. 하지만 state는 실제로 리액트 내부에 보관된다. 리액트는 UI 트리에서 해당 컴포넌트의 위치에 따라 보유하고 있는 각 state를 올바른 컴포넌트와 연결한다.

 

아래의 코드에서 <Counter /> JSX 태그는 오직 한번만 존재하지만, 서로 다른 위치에 렌더링된다.

export default function App() {
  const counter = <Counter />;
  return (
    <div>
      {counter}
      {counter}
    </div>
  );
}

 

트리로 보면 아래와 같다.

 

트리에서 각각 고유한 위치에 렌더링되기 때문에, 두 가지 Counter들은 분리되어 있다.

이는 일반적으로 리액트를 사용하기 위해 필수적으로 알아야하는 개념은 아니지만, 어떻게 동작하는지를 이해하는 데 유용하다.

 

리액트에서 화면의 각 컴포넌트는 완전히 격리된 state를 가진다.

  • 예를 들어, 두 Counter 컴포넌트를 나란히 렌더링하게 되면, 각각 고유한 독립적인 scorehover state를 가진다.

그래서 위의 코드 결과에서 Counter를 각각 눌러보면 서로 영향을 주지 않는 것을 확인할 수 있다.

 

리액트는 같은 위치에서 같은 컴포넌트를 렌더링하는 한 state는 유지된다.

이를 확인하기 위해 (1) 두 Counter를 증가시키고, (2)"Render the secound counter" 체크 박스에 체크를 없애 두번째 컴포넌트만 지운 후, (3) 다시 체크를 해서 두번째 컴포넌트를 추가해보자.

  • (2) 두 번째 컴포넌트에 대한 렌더링을 중단하는 순간, 해당 state는 완전히 사라진다!
    • 리액트는 컴포넌트가 지워지면 그 state도 사라지기 때문에 이러한 현상이 발생한다.
  • (3) 다시 체크 박스를 클릭하면, 두 번째 컴포넌트가 렌더링되고, 그 state가 처음부터 초기화되고 DOM에 추가된다.

 

즉, 리액트는 컴포넌트가 UI 트리에서 해당 위치에 렌더링되는 한 그 state를 보존한다. 만약 제거되거나 같은 위치에 다른 컴포넌트가 렌더링된다면, 리액트는 해당 컴포넌트의 state를 버린다.

동일 위치에 같은 컴포넌트가 있을 경우, state가 보존(유지)한다.

이 예제에서 두가지 다른 Counter tag가 있다.

 

체크 박스를 여러번 눌러도, counter state는 리셋되지 않는다. isFancytrue인지 false인지와 상관없이, 최상위 App 컴포넌트로부터 반환된 div의 첫번째 자식으로 항상 <Counter />를 가지기 때문이다.

 

App state를 변경하더라도 Counter는 동일한 위치에 존재하기 때문에 그 상태를 초기화하지 않느다.

Pitfall : 중요한 것은 JSX 마크업이 아니라 UI 트리에서의 위치라는 것을 기억하자!

아래 컴포넌트는 if를 기준으로 두가지 다른 <Counter />를 반환한다.

체크 박스를 누르면 state가 리셋되기를 기대하겠지만, 이는 틀렸다! 두 <Counter /> 태그가 모두 같은 위치에 렌더링되기 때문이다.

리액트는 함수 내 조건에 따라 어디에 위치하는 지를 알 수 없다. 리턴하는 tree 만 "볼 수"있다.

두 경우 모두 App 컴포넌트가 첫번째 자식으로 <Counter />를 가진 <div>를 리턴하기 때문에 리액트가 동일한 <Counter />로 간주한다.

루트의 첫번째 자식의 첫번째 자식이 같은 "주소"를 갖는다고 생각하면 된다.

이는 로직을 어떻게 구성하든 상관없이 리액트가 이전과 다음 렌더링을 매칭하는 방법이다.

동일한 위치에 있는 다른 컴포넌트가 있을 경우, state가 초기화된다.

동일한 위치에서 다른 컴포넌트(<p>)로 전환하는 것은 기존 컴포넌트를 제거 후 다른 컴포넌트를 렌더링하는 것과 같다.

  • 기존 컴포넌트를 제거할 때, 그 아래의 전체 트리(컴포넌트, state, ...)도 모두 제거된다.

즉, 동일한 위치에서 다른 컴포넌트로 전환하면 기존 컴포넌트의 state가 초기화된다.

  • 컴포넌트 타입이 달라도 동일하게 적용된다.

리렌더링 사이에 state를 유지하려면, 트리 구조가 렌더링할 때마다 일치해야한다.

  • 만약 구조가 다르다면, 위에서 말한 것처럼 리액트는 트리에서 컴포넌트가 제거될 때 state를 제거하므로 state는 제거된다.

예) 체크 박스를 클릭시 <Counter><p> 전환

카운터 내 버튼을 통해 카운터의 점수를 올린 후 체크 박스를 두 번 클릭하면, 카운터가 새로 렌더링될 때 점수는 0으로 초기화되는 것을 볼 수 있다.

pitfall : 컴포넌트 함수 정의를 중첩시키면 안된다.

  • 컴포넌트 함수 정의 중첩이란?
    • 컴포넌트 함수 정의 내부에 다른 컴포넌트 함수를 정의하는 것

문제점 : 의도치 않은 버그가 생기거나 성능 문제가 발생한다.

  • 컴포넌트 함수 정의가 중첩되면 상위 컴포넌트의 렌더링될 때(=상태 변경될 때)마다 내부에 정의된 컴포넌트 함수가 새로 생성된다. 이로 인해 내부에 정의된 컴포넌트의 상태가 초기화된다.이 때문에 버그나 성능 문제가 발생하게 된다.
  • 예) MyTextField 컴포넌트가 MyComponent 컴포넌트 내부에 정의
    • 버튼을 누르면 MyComponent 컴포넌트의 counter state가 변경되며 리렌더링이 발생한다. 이때 MyComponent가 새로 생성되면서, 내부의 input state가 초기화된다. 따라서 버튼을 누를 때마다 input state가 사라진다.

해결 방법 : 컴포넌트 함수를 최상위 레벨에서 선언하고 정의를 중첩시키지 않아야 한다.

 

동일한 위치에서 state를 초기화하기

기본적으로 리액트는 동일한 위치에 있는 컴포넌트의 state를 보존한다. 가끔 동일한 위치의 컴포넌트의 state를 초기화하고 싶은 때가 있다.

 

예) 두 명의 플레이어가 매 턴 동안 점수를 추적할 수 있는 앱

  • 현재 플레이어가 변경되어도 점수가 변경되지 않고 유지되는 문제가 있다.
    • Counter 모두 동일한 위치에 나타나기 때문에, 리액트에서는 동일한 Counterperson prop만 변경되는 것으로 인식하기 때문이다.

 

만약 컴포넌트 state를 초기화하고 싶으면, 아래 두 가지 방법 중 하나를 사용할 수 있다.

  1. 다른 위치에 컴포넌트를 렌더링한다.
  2. 각 컴포넌트에 명시적 고윳값인 key를 부여한다.

옵션 1 : 다른 위치에 컴포넌트 렌더링하기

두 개의 Counter를 독립적으로 만드려면, 서로 다른 위치에 렌더링하면된다.

 

즉, 위 예시를 해결하기위해 아래와 같이 코드를 수정하면 된다.(전체 코드 보기)

{isPlayerA &&
  <Counter person="Taylor" />
}
{!isPlayerA &&
  <Counter person="Sarah" />
}
  • isPlayerAtrue이면, 첫 번째 위치에는 Counter가 포함되고, 두 번째 위치는 비어있다.
  • "Next player" 버튼을 클릭하면, 첫 번째 위치는 지워지고, 두 번째 위치에 Counter가 포함된다.

Counter의 state는 DOM에서 제거될 때마다 소멸되기 때문에 버튼을 클릭할 때마다 state가 초기화된다.

 

이 해결 방법은 동일한 위치에서 소수의 독립적인 컴포넌트를 렌더링하고 싶을 때 편리하다.

  • 예시에서는 독립적으로 렌더링할 컴포넌트가 두개 뿐이여서 이 방법을 사용하는 것이 번거롭지 않다.

옵션 2 : key를 사용해 state를 초기화하기

이 방법은 특히 form을 다룰 때 유용하다.

 

예) 왼쪽(ContactList)에 수신자를 선택하고 오른쪽(Chat)에 input과 보내기 버튼이 존재하는 채팅 앱

  • input에 내용을 추가한 후, 왼쪽에 수신자를 변경할 때 input의 내용이 초기화되지 않는 문제가 있다.

key를 추가해 문제를 해결할 수 있다.

<Chat key={to.id} contact={to} />

다른 수신자를 선택할 때, Chat 컴포넌트가 그 아래 트리의 모든 state를 포함해 처음부터 다시 생성된다. 리액트는 DOM 엘리먼트를 재사용하는 대신 다시 생성한다.

  • 현재 예제는 수신자에 따라 input state를 가지므로, 수신자를 기준으로 key를 부여하는 것이 합리적이다.

DEEP DIVE : 컴포넌트가 지워지더라도 state를 유지하기

실제 채팅 앱에서는 사용자가 이전 수신자를 다시 선택할 때 input state를 복구하고 싶을 수도 있다. 이를 가능하게 하는 몇 가지 방법이 있다.

  • 모든 채팅을 다 렌더링하고, CSS를 사용해 숨길 수 있다.
    • 트리에서 채팅이 지워지지 않아 각 state도 유지된다.
    • 특징
      • 간단한 UI를 만들고 싶을 때 좋다.
      • 숨겨진 트리가 많거나 많은 DOM 노드를 포함할 때 매우 느릴 수 있다.
  • 각 수신자에서 작성한 메시지의 state를 상위 컴포넌트로 끌어올려서 보관할 수 있다.
    • 상위 컴포넌트가 정보를 보관하므로 하위 컴포넌트가 지워더라도 state가 유지된다.
    • 특징
      • 가장 일반적인 해결책
  • 리액트 state외 그 밖의 소스를 사용할 수 있다.
    • 예를 들어 사용자가 실수로 페이지를 닫아도 메시지가 유지되기를 원할 수 있다. 이를 구현하기 위해 localStorage를 읽어 상태를 초기화하고 초안도 저장하도록 만들 수 있다.

https://react.dev/learn/conditional-rendering을 읽고 정리한다.

컴포넌트는 종종 조건에 따라 다른 것을 보여줄 필요가 있다.

리액트에서는 JS 문법(if, &&, ? :)을 사용해 조건에 따라 JSX를 렌더링 할 수 있다.

조건에 따라 JSX를 반환하기

PackingList 컴포넌트는 여러 Item들을 렌더링 한다.

Item 컴포넌트들은 isPacked prop이 true가 되면 checkmark(✔)를 추가하고 싶다고 하자.
이는 if/else 문 사용해 해결할 수 있다.

if (isPacked) {
  return <li className="item">{name} ✔</li>;
}
return <li className="item">{name}</li>;

 

조건에 따라 아무것도 반환하지 않기(null)

몇 가지 상황에서, 아무것도 렌더링 하고 싶지 않을 수도 있다. 예를 들어, isPacked===true인 아이템을 보여주고 싶지 않을 수도 있다. 하지만 컴포넌트는 항상 무언가를 반환해야 하므로, 이 경우 null을 리턴할 수 있다.

if (isPacked) {
  return null;
}
return <li className="item">{name}</li>;

실제로 null을 반환하는 것은 일반적이지 않다(렌더링 하려는 개발자를 놀라게 할 수 있기 때문ㅜㅜ).
더 자주 사용되는 방법으로, 부모 컴포넌트의 JSX에서 조건에 따라 특정 컴포넌트를 포함하거나 제외하는 방법이 사용된다. 그 내용은 다음 문단에서 설명하겠다.

조건에 따라 JSX를 포함하기

이전 예제에서는 컴포넌트를 반환하는 것으로 JSX 트리를 컨트롤했다. 렌더 출력에서 아래와 같은 중복이 발생한다.
아래 두 가지 코드는 거의 비슷하다.

<li className="item">{name} ✔</li>
<li className="item">{name}</li>

이러한 중복은 최악은 아니지만, 코드를 유지 보수하기 어렵게 만들 수 있다.

  • 만약 className을 변경하고 싶다면? 두 번 고쳐야 한다!

반복 작업을 제거하기 위해 JSX에서 조건 연산자를 사용할 수 있다.

조건 연산자 사용하기(? :)

isPacked === true이면, name + ' ✔'를 렌더링 하고, 아니면 name을 렌더링 한다.

<개선 전>

if (isPacked) {
  return <li className="item">{name} ✔</li>;
}
return <li className="item">{name}</li>;

<개선 후>

return (
  <li className="item">
    {isPacked ? name + ' ✔' : name}
  </li>
);

다른 HTML 태그에서 item의 텍스트를 더 복잡하게 만들고 싶다고 하자.
케이스마다 많은 JSX 중첩을 쉽게 하기 위해 줄 바꿈과 괄호를 사용해 추가할 수 있다.


이 스타일은 간단한 조건일 때 적합하다. 만약 컴포넌트가 조건을 가진 마크업이 중첩되고 너무 많으면 지저분해지기 때문에 자식 컴포넌트를 추출하여 정리하는 것을 고려해야 한다. React에서 마크업은 코드의 일부임으로 변수나 함수와 같은 도구를 사용해 복잡한 표현식을 정리할 수 있다.

And 연산자(&&)

또 다른 일반적인 방법은 JS의 AND 연산자를 사용하는 것이다. 리액트 컴포넌트들에서, 조건이 true일 때만 아니면 일부 JSX를 렌더링 하고 그렇지 않으면 아무것도 렌더링 하지 않을 때 자주 사용된다.
&&를 사용해 조건적으로 isPackedtrue일 때 체크 마크를 렌더링 할 수 있다.

return (
  <li className="item">
    {name} {isPacked && '✔'}
  </li>
);

&& 문법은 왼쪽의 조건이 true일 때만 오른쪽의 값을 반환한다. 하지만, 왼쪽의 조건이 false라면, 전체 조건식을 false가 된다.
리액트는 false를 JSX 트리에서 "hole"로 인식하며, 이는 null 또는 undefined와 같다. 그리고 이 부분은 아무것도 렌더링 하지 않는다.

Pitfall : && 왼편에 숫자를 넣지 마라.

조건을 테스트하기 위해, JS는 자동적으로 왼편을 불리언으로 변환한다.
그러나, 왼편에 0이 있으면 리액트는 0을 렌더링 하게 된다.

예를 들어, 흔히 하는 실수로 messageCount && <p>New messages</p>같이 코드를 작성한다.
이 코드는 messageCount0일 때 아무것도 렌더링 하지 않는 것으로 생각하지 싶지만, 실제로는 0 자체를 렌더링 한다.

따라서 고치기 위해, 왼편을 불리언 값으로 만들어야 한다.
=>messageCount > 0 && <p>New messages</p>

변수를 이용해 조건적으로 JSX를 할당하기

코드를 작성할 때 위의 간단한 문법이 방해가 된다면, if문과 변수를 사용할 수 있다.

  1. 변수를 정의할 때 let사용하면 값을 재정의할 수 있으므로, 처음엔 보여주고 싶은 기본 내용(name)을 할당한다.
  2. let itemContent = name;
  3. if문을 사용해 isPackedtrue일 때만 itemContent를 재할당한다.
  4. if (isPacked) { itemContent = name + " ✔"; }
  5. 반환할 JSX 트리 안에 중괄호를 사용해 변수를 추가한다.(중괄호를 사용해 JS를 문법을 사용 가능)
  6. <li className="item"> {itemContent} </li>

이 스타일은 이전보다 간단하진 않지만, 가장 유연하다.

이전에는 텍스트만 사용했지만, JSX도 가능하다.

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와 함께 사용되는 이유이다.

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

 

 

+ Recent posts