React 공식 문서 읽어보기 (2)

Snoop So·2023년 7월 22일
0

객체 상태 업데이트

  • Immer를 쓰라고 무려 공식 문서가 추천을 해준다 쓰자
  • [] Immer 사용?
import { useImmer } from 'use-immer';

export default function Form() {
  const [person, updatePerson] = useImmer({
    name: 'Niki de Saint Phalle',
    artwork: {
      title: 'Blue Nana',
      city: 'Hamburg',
      image: 'https://i.imgur.com/Sd1AgUOm.jpg',
    }
  });

  function handleNameChange(e) {
    updatePerson(draft => {
      draft.name = e.target.value;
    });
  }

  function handleTitleChange(e) {
    updatePerson(draft => {
      draft.artwork.title = e.target.value;
    });
  }

  function handleCityChange(e) {
    updatePerson(draft => {
      draft.artwork.city = e.target.value;
    });
  }

  function handleImageChange(e) {
    updatePerson(draft => {
      draft.artwork.image = e.target.value;
    });
  }

  return (
    <>
      <label>
        Name:
        <input
          value={person.name}
          onChange={handleNameChange}
        />
      </label>
      <label>
        Title:
        <input
          value={person.artwork.title}
          onChange={handleTitleChange}
        />
      </label>
      <label>
        City:
        <input
          value={person.artwork.city}
          onChange={handleCityChange}
        />
      </label>
      <label>
        Image:
        <input
          value={person.artwork.image}
          onChange={handleImageChange}
        />
      </label>
      <p>
        <i>{person.artwork.title}</i>
        {' by '}
        {person.name}
        <br />
        (located in {person.artwork.city})
      </p>
      <img 
        src={person.artwork.image} 
        alt={person.artwork.title}
      />
    </>
  );
}

배열 상태 업데이트

  • 삽입하기 항상 어려워하는데 이 예제 좋다.
import { useState } from 'react';

let nextId = 3;
const initialArtists = [
  { id: 0, name: 'Marta Colvin Andrade' },
  { id: 1, name: 'Lamidi Olonade Fakeye'},
  { id: 2, name: 'Louise Nevelson'},
];

export default function List() {
  const [name, setName] = useState('');
  const [artists, setArtists] = useState(
    initialArtists
  );

  function handleClick() {
    const insertAt = 1; // Could be any index
    const nextArtists = [
      // Items before the insertion point:
      ...artists.slice(0, insertAt),
      // New item:
      { id: nextId++, name: name },
      // Items after the insertion point:
      ...artists.slice(insertAt)
    ];
    setArtists(nextArtists);
    setName('');
  }

  return (
    <>
      <h1>Inspiring sculptors:</h1>
      <input
        value={name}
        onChange={e => setName(e.target.value)}
      />
      <button onClick={handleClick}>
        Insert
      </button>
      <ul>
        {artists.map(artist => (
          <li key={artist.id}>{artist.name}</li>
        ))}
      </ul>
    </>
  );
}
  • reverse(), sort()는 원래 배열을 변이하므로 사용하면 안됨, 대신 먼저 복사해서 사용하기
  • 대신 깊은 복사는 안됨

  function handleClick() {
    const nextList = [...list];
    nextList.reverse();
    setList(nextList);
  }
  • 깊은 복사의 안좋은 예제 vs 좋은 예제
// bad
const myNextList = [...myList];
const artwork = myNextList.find(a => a.id === artworkId);
artwork.seen = nextSeen; // Problem: mutates an existing item
setMyList(myNextList);

// good
setMyList(myList.map(artwork => {
  if (artwork.id === artworkId) {
    // Create a *new* object with changes
    return { ...artwork, seen: nextSeen };
  } else {
    // No changes
    return artwork;
  }
}));

상태

  • 다양한 상태의 종류들
    비어있음: form의 “Submit”버튼은 비활성화되어 있습니다.
    입력중: form의 “Submit”버튼이 활성화되어 있습니다.
    제출중: form은 완전히 비활성화되어있고 Spinner가 표시됩니다.
    성공시: form 대신 “Thank you”메세지가 표시됩니다.
    실패시: ‘입력중’ 상태와 동일하지만 추가로 오류 메세지가 표시됩니다.

  • 상태 변경을 촉발하는 요인들
    사람의 입력 : 버튼 클릭, 필드 입력, 링크 이동 등
    컴퓨터의 입력 : 네트워크에서 응답 도착, 시간 초과, 이미지 로딩 등

  • "가장 좋은 방법을 즉시 생각하기 어렵다면 가능한 모든 시각적 상태를 확실하게 다룰 수 있을 만큼 충분한 state를 추가하는 것부터 시작하세요."

  • 필수가 아닌 state 제거하는 원칙

  1. state가 모순을 야기하나요? 예를 들어, isTyping 과 isSubmitting은 동시에 true일 수 없습니다. 이러한 모순은 일반적으로 state가 충분히 제약되지 않았음을 의미합니다. 두 boolean의 조합은 네 가지가 가능하지만 유효한 state는 세 가지뿐입니다. “불가능한” state를 제거하려면 세 가지 값을 하나의 status로 결합하면 됩니다: 'typing', 'submitting', 'success'.
  2. 다른 state 변수에 이미 같은 정보가 있나요? isEmpty와 isTyping은 동시에 true가 될 수 없습니다. 이를 각 state 변수로 분리하면 동기화되지 않아 버그가 발생할 위험이 있습니다. 다행히 isEmpty를 제거하고 대신 answer.length === 0으로 확인할 수 있습니다.
  3. 다른 state 변수를 뒤집으면 동일한 정보를 얻을 수 있나요? isError는 error !== null을 대신 확인할 수 있기 때문에 필요하지 않습니다.
    [] Challenge 3 of 3: Refactor the imperative solution without React (https://react-ko.dev/learn/reacting-to-input-with-state)

Reducer

  • reducer는 state를 관리하는 다른 방법임
  • state 를 직접 설정하는 것과는 약간 다름
  • state를 설정하여 React에게 "무엇을 할 지" 지시하는 대신, 이벤트 핸들러에서 "action"을 전달하여 "사용자가 방금 한 일"을 지정함
  • 즉, 태스크 설정 대신 태스크를 추가/변경/삭제하는 action을 전달하는 것
  • dispatch (급파하다)
  • reducer 인 이유-> reduce()에서 따옴. 배열을 가지고 많은 값을 하나의 값으로 누적할 수 있음
  • reducer로 넘기는 함수가 reducer임
  • 핵심은 관심사의 분리임

useState vs useReducer

  • useState가 코드 양은 줄어듦, 가독성 좋음
  • useState는 디버깅이 어려움, useReducer는 reducer에 콘솔 로그를 추가해 모든 state 업데이트와 왜 버그가 발생했는지 확인 가능
  • reducer가 테스팅에도 유리

state 구조화 원칙

  • 관련 state를 그룹화합니다. 항상 두 개 이상의 state 변수를 동시에 업데이트하는 경우 단일 state 변수로 병합하는 것이 좋습니다. (포지션 x,y의 경우 묶기)
  • state의 모순을 피하세요. 여러 state 조각이 서로 모순되거나 ‘불일치’할 수 있는 방식으로 state를 구성하면 실수가 발생할 여지가 생깁니다. 이를 피하세요. (isSending, isSent가 동시에 true가 되는 상황 자체를 막기 위해 status로 변경하고 'typing', 'sending', 'sent'를 담기
  • 불필요한 state를 피하세요. 렌더링 중에 컴포넌트의 props나 기존 state 변수에서 일부 정보를 계산할 수 있다면 해당 정보를 해당 컴포넌트의 state에 넣지 않아야 합니다.
  • state 중복을 피하세요. 동일한 데이터가 여러 state 변수 간에 또는 중첩된 객체 내에 중복되면 동기화 state를 유지하기가 어렵습니다. 가능하면 중복을 줄이세요.
function Message({ messageColor }) {
  const [color, setColor] = useState(messageColor);
 // 이거 하지 말기

selectedItem이 아닌, selectedId로 상태 저장하기

  • 깊게 중첩된 state는 피하세요. 깊게 계층화된 state는 업데이트하기 쉽지 않습니다. 가능하면 state를 평평한 방식으로 구성하는 것이 좋습니다.
    차라리 정규화를 해라. flat!!! 공부해보자.

위와 같은 원칙은 데이터베이스 구조를 정규화하는 것과 비슷함.
"state를 최대한 단순하게 만들되, 그보다 더 단순해서는 안됩니다."

컴포넌트 간의 state 공유

  1. 자식 컴포넌트에서 state를 제거합니다.
  2. 공통 부모 컴포넌트에 하드 코딩된 데이터를 전달합니다.
  3. 공통 부모 컴포넌트에 state를 추가하고 이벤트 핸들러와 함께 전달합니다.

state를 끌어올리려면 조정하려는 두자식 컴포넌트의 가장 가까운 공통 부모 컴포넌트를 찾아야 함
이는 source of truth가 됨

SSOT

Don't repeat yourself

https://en.wikipedia.org/wiki/Don%27t_repeat_yourself

React 어떻게 설계할까

  1. UI 컴포넌트 계층 구조로 나누기
  2. React로 정적인 UI 만들기
  3. 최소한의 완전한 UI state 찾기
  4. state가 어디에 있어야 할지 파악하기
  5. 역방향 데이터 흐름 추가하기
1. 해당 state를 기반으로 렌더링하는 모든 컴포넌트를 찾으세요.
2. 가장 가까운 공통 상위 컴포넌트, 즉,계층상 그 state의 영향을 받는 모든 컴포넌트들의 위에 있는 컴포넌트를 찾으세요.
3. state가 어디에 위치할지 결정합시다:
4. 대개 공통 부모에 state를 그대로 둘 수 있습니다.
5. 혹은 공통 부모보다 더 상위 컴포넌트에 state를 둘 수도 있습니다.
6. state를 소유할 적절한 컴포넌트를 찾지 못했다면, state를 소유하는 새 컴포넌트를 만들어 공통 부모 컴포넌트보다 상위에 추가하세요.

state 구조 선택

  • 만약 동일한 컴포넌트이지만 렌더링이 다시 되어야 하는 경우, key를 state에 연결하면 재렌더링이 된다. ()

state 보존 및 재설정

  • state는 트리의 한 위치에 묶인다.
  • state는 각 컴포넌트 내부가 아닌 React 내부에 있음. 해당 컴포넌트가 어디에 위치해있느냐에 따라 보유하고 있는 각 state를 컴포넌트와 연결함
  • 해당 컴포넌트가 사라지면 그 state도 사라지게 됨
  • 동일한 위치의 동일한 컴포넌트는 state를 유지함
  • React에서 중요한 것은 JSX 마크업이 아니라 UI 트리에서의 위치임
  • React는 함수에서 조건을 어디에 배치했는지 알지 못함. 단지 반환하는 트리만 볼 수 있을 뿐임. Counter 태그가 계속해서 같은 위치에 렌더링 되기 때문에 변경된 것을 알지 못함
  • 같은 위치에 다른 컴포넌트를 렌더링하면 전체 하위 트리의 state가 재설정됨
  • 리렌더링되더라도 state가 유지되려면 트리의 구조가 일치해야 함
  • 그래서 컴포넌트 함수 정의를 중첩하면 안되는 것

ref

  • const ref = useRef(0); 이 ref는 아래의 값을 반환함
{ 
  current: 0 // The value you passed to useRef
}
  • current를 통해 해당 ref의 현재 값에 액세스
  • 렌더링에 관여하지 않는 정보를 ref에 저장하면 좋음 (interval id 같은거)
  • 객체지향 프로그래밍에 익숙하다면 인스턴스 필드를 떠올릴 수 있는데, this.something 대신 somethingRef.current를 사용
  • if (!ref.current) ref.current = new Thing()
  • ref는 즉시 변경
  • map 돌리는데에서 ref 자동 지정하려면 함수를 쓸 수 있음
{catList.map(cat => (
  <li
    key={cat.id}
    ref={(node) => {
      const map = getMap();
      if (node) {
        map.set(cat.id, node);
      } else {
        map.delete(cat.id);
      }
    }}
    >
    <img
      src={cat.imageUrl}
      alt={'Cat #' + cat.id}
      />
  </li>
))}

번외로 왜 나는 이 API를 몰랐을까 충격이다..

useEffect

  • effect: 렌더링으로 인해 발생하는 사이드 이펙트를 말함
  • 정말 필요한지 다시 한번 생각해볼것
  • 목록이 변경될 때 state 변수를 업데이트하는 effect를 작성하는 것은 비효율적
  • effect가 즉시 state를 업데이트 한다면 이로 인해 전체 프로세스가 처음부터 다시 실행됨
  • 사용자의 이벤트를 처리하는데에 effect는 필요 없음
  • 외부 시스템과 동기화하기 위해 effect가 필요함
  • Effect가 무언인가 패치하면 클린업 함수는 패치를 중단하거나 그 결과를 무시해야 함

Effect에서 데이터 패칭하는 것의 대안

  • Effects는 서버에서 실행되지 않음. 즉, 초기 서버에서 렌더링되는 HTML에는 데이터가 없는 로딩 state만 포함됨. 클라이언트 컴퓨터는 모든 자바스크립트를 다운로드 하고 앱을 렌더링 한 후에야 비로소 데이터를 로드함. Effect에서 직접 패치하면 네트워크 워터폴이 만들어지기 쉬움. 프레임워크를 사용하는 경우 빌트인 데이터 패칭 매커니즘을 사용. 혹은 라이브러리 React Query, useSWR, React Router 등을 사용하면 효율적으로 데이터 패칭이 가능해짐.

0개의 댓글