React를 하면서 내가 놓쳤던 것들 2편

박정훈·2022년 12월 5일
1

React

목록 보기
4/10

몇일 동안 머리 싸매고 한 기업과제 두개를 어제 끝냈다. 꽤 재밌었고 힘들었다.
방금 또 한 기업에서 리액트 과제를 하자고 연락이 왔다. 아이고.. 어쩌랴 해야지. 그래도 오늘은 공부 할꺼야..

Error Boundaries부터 다시..

에러 바운더리

UI의 일부분에만 영향을 끼치는 자바스크립트 에러가 전체 앱을 중단시켜서는 안된다.

에러 바운더리란 하위 컴포넌트 트리 어디서든 자바스크립트 에러를 기록하며 깨진 컴포넌트 트리 대신에 fallback UI를 보여주는 리액트 컴포넌트다.

에러 바운더리는 아래 에러는 포착하지 않는다.

  • 이벤트 핸들러
  • 비동기적 코드(setTimeout 혹은 requestAnimationFrame 콜백)
  • SSR
  • 자식에서가 아닌 에러 바운더리 자체에서 발생하는 에러

자바스크립트의 catch{} 구문과 유사한데, 컴포넌트에 적용되는 것이다. 오직 클래스 컴포넌트만이 에러 바운더리가 될 수 있다.

에러 바운더리 자체적으로 에러를 포착할 수 없고, 에러 바운더리가 에러 메시지를 렌더링하는 데에 실패한다면 에러는 그 위의 가장 가까운 에러 경계로 전파된다.

에러 바운더리의 위치

에러 경계의 좀 더 세분화된 부분은 개발자의 몫이다. 최상위 경로의 컴포넌트를 감쌀수도 있고, 에러 경계의 각 위젯을 에러 경계로 감싸서 앱의 나머지 부분이 충돌하지 않도록 보호할 수 도 있다.

포착되지 않는 에러

React16부터는 에러 바운더리에서 포착되지 않은 에러로 인해 전체 React 컴포넌트 트리의 마운트가 해제된다.
손상된 UI는 제거하는게 좋은데, 메신저 같은경우 손상된 UI를 계속 제공한다면, 잘못된 사람에게 메시지를 보낼수도 있고, 결제 앱에서 잘못된 금액을 계속 보여준다면... 이 또한 더 안좋은 상황이라고 할 수 있다.

react-error-boundary를 읽어보면 왜 해당 라이브러리를 사용하면 좋은지를 설명해주고 있다.

억지로 예제를 만들어봤다 하하..

// App.jsx
import { useState } from "react";
import { ErrorBoundary } from "react-error-boundary";
import "./App.css";
import ErrorFallback from "./components/error-boundary/ErrorFallback";
import Pokemon from "./components/pokemon/Pokemon";

function App() {
  const [user, setUser] = useState({
    pokemon: "피카츄",
  });

  return (
    <ErrorBoundary
      FallbackComponent={ErrorFallback}
      onReset={() => {
        setUser((cur) => {
          console.log(cur);
          return {
            ...cur,
            name: "지우",
          };
        });
      }}
      resetKeys={[user]}
    >
      <Pokemon name={user.name} />
    </ErrorBoundary>
  );
}
//ErrorFallback.jsx
import React from "react";

const ErrorFallback = ({ error, resetErrorBoundary }) => {
  return (
    <div role="alert">
      <p>문제 발생!</p>
      <pre style={{ color: "red" }}>{error.message}</pre>
      <button onClick={resetErrorBoundary}>다시 시도해봐!</button>
    </div>
  );
};

export default ErrorFallback;
// Pokemon.jsx
const Pokemon = ({ name }) => {
  const [pokemons, setPokemons] = useState([]);

  const handleOnClick = async () => {
    try {
      const result = await (
        await fetch("https://pokeapi.co/api/v2/pokemo/")
      ).json();
      setPokemons(result.results);
    } catch (e) {
      throw e;
    }
  };

  return (
    <>
      <div>환영해요!{name.toUpperCase()}!</div>
      <button onClick={handleOnClick}>포켓몬 정보 내놔!</button>
      {pokemons.length > 0 &&
        pokemons.map((pokemon) => (
          <PokemonCard name={pokemon.name} url={pokemon.url} />
        ))}
    </>
  );
};

컴포넌트 스택 추적

React16은 컴포넌트 스택 추적을 제공한다. 정확하게 컴포넌트 트리 어느 부분에서 에러가 발생했는지 알 수 있다.
스택 추적에 표시되는 컴포넌트 이름은 Function.name 프로퍼티에 따라 다르다.

Ref 전달하기

컴포넌트를 통해 자식중 하나에게 ref를 자동으로 전달하는 기법이다. 일반적으로 애플리케이션 대부분의 컴포넌트에 필요하지는 않다. 그렇지만, 특히 재사용 가능한 컴포넌트 라이브러리와 같은 어떤 컴포넌트에서는 유용할 수 있다.

DOM에 refs 전달하기

포커스, 선택, 애니메이션을 관리하기 위해서는 DOM노드에 접근하는 것이 불가피 할 수 있다.
Ref 전달하기는 일부 컴포넌트가 수신한 ref를 받아 조금 더 아래로 전달할 수 있는 옵트인 기능이다.

// App.jsx
function App() {
  const ref = createRef();

  useEffect(() => {
    ref.current?.focus();
  }, []);
  return (
    <>
      <div>자동 포커싱</div>
      <Input ref={ref} />
    </>
  );
}

//Input.jsx
import { forwardRef } from "react";

const Input = (props, ref) => <input ref={ref} />;

export default forwardRef(Input);

Input은 React.forwardRef를 사용해 전달된 ref를 얻고, 그것을 렌더링 되는 DOM input으로 전달한다.
이제 Input을 사용하는 컴포넌트들은 input DOM 노드에 대한 참조를 가져올 수 있고, 필요한 경우 DOM button을 직접 사용하는 것처럼 접근할 수 있다!

JSX 이해하기

근본적으로 JSX는 React.createElement(component, props, ...children) 함수에 대한 syntatic sugar를 제공할 뿐입니다.

JSX 타입을 위한 점 표기법 사용

JSX 내에서도 점 표기법을 사용하여 React 컴포넌트를 참조할 수 있다.

// Table/index.jsx
import { default as Table } from "./Table";
import { default as TableHead } from "./TableHead";
import { default as TableBody } from "./TableBody";

const _Table = Table;
_Table.Head = TableHead;
_Table.Body = TableBody;

const TableComponent = _Table;

export default TableComponent;

// Table/Table.jsx
const Table = ({ children }) => {
  return <table>{children}</table>;
};

// Table/TableHead.jsx
const TableHead = ({ children }) => {
  return (
    <thead>
      <tr>
        <th>{children}</th>
      </tr>
    </thead>
  );
};

// Table/TableBody.jsx
const TableBody = ({ children }) => {
  return (
    <tbody>
      <tr>
        <td>{children}</td>
      </tr>
    </tbody>
  );
};

//App.jsx
function App() {

  return (
    <>
      <TableComponent>
        <TableComponent.Head>타이틀</TableComponent.Head>
        <TableComponent.Body>바디</TableComponent.Body>
      </TableComponent>
    </>
  );
}

함수를 자식으로 사용하기

const Repeat = (props) => {
  let items = [];
  for (let i = 0; i < props.numTimes; i++) {
    items.push(props.children(i));
  }
  return <div>{items}</div>;
}

const ListOfTenThings = () => {
  return ( 
    <Repeat numTimes={10}>
      {(index) => <div key={index}>This is item {index} in the list</div>}
    </Repeat>
  );
}

// result
This is item 0 in the list
This is item 1 in the list
This is item 2 in the list
This is item 3 in the list
This is item 4 in the list
This is item 5 in the list
This is item 6 in the list
This is item 7 in the list
This is item 8 in the list
This is item 9 in the list

Portals

Portal은 부모 컴포넌트의 DOM 계층 구조 바깥에 있는 DOM 노드로 자식을 렌더링하는 최고의 방법을 제공합니다.

ReactDOM.createPortal(child, container)

첫 번째 인자(child)는 엘리먼트, 문자열, 혹은 fragment와 같은 어떤 종류이든 렌더링할 수 있는 React 자식이다. 두 번째 인자(container)는 DOM 엘리먼트다.

사용법

보통 컴포넌트 렌더링 메서드에서 엘리먼트를 반환할 때 그 엘리먼트는 부모 노드에서 가장 가까운 자식으로 DOM에 마운트된다.

render() {
  // React는 새로운 div를 마운트하고 그 안에 자식을 렌더링한다.
  return (
    <div>
      {this.props.children}
    </div>
  );
}

그런데 가끔 DOM의 다른 위치에 자식을 삽입하는 것이 유용할 수 있다.

render() {
  // React는 새로운 div를 생성하지 *않고* `domNode` 안에 자식을 렌더링한다.
  // `domNode`는 DOM 노드라면 어떠한 것이든 유효하고, 그것은 DOM 내부의 어디에 있든지 상관없다!
  return ReactDOM.createPortal(
    this.props.children,
    domNode
  );
}

세상에! poratal의 전형적인 유스케이스는 부모 컴포넌트에 overflow: hidden이나 z-index가 있는 경우이지만, 시각적으로 자식을 “튀어나오도록” 보여야 하는 경우라고 한다. 예를 들면, 다이얼로그, 호버카드나 툴팁과 같은 것들이라고 한다.

모달도 가능하려나?

Portal을 통한 이벤트 버블링

portal이 DOM트리의 어디에도 존재할 수 있다 하더라도 모든 다른 면에서 일반적인 React 자식처럼 동작한다. context와 같은 기능은 자식이 portal이든지 아니든지 상관하지 않는다! DOM 트리에서의 위치에 상관없이 portal은 여전히 React 트리에 존재하기 때문이다.

이는 이벤트 버블링에서도 마찬가지로 통용된다.

말해 뭐하랴. 직접 써봐야지.

// index.html
...
  <body>
    <div id="root"></div>
    <div id="modal-root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>

//ModalPortal.jsx
import ReactDOM from "react-dom";

const ModalPortal = ({ children }) => {
  const modalRoot = document.getElementById("modal-root");
  return ReactDOM.createPortal(children, modalRoot);
};

export default ModalPortal;


// Modal
const Modal = ({ children, setModalClose }) => {
  return (
    <ModalPortal>
      <div className="back" onClick={() => setModalClose(false)}>
        <div className="container">{children}</div>
      </div>
    </ModalPortal>
  );
};

// App.jsx
function App() {
  const [modal, setModal] = useState(false);

  return (
    <>
      <button onClick={() => setModal(true)}>모달 열기</button>
      {modal && (
        <Modal
          setModalClose={() => {
            setModal(false);
          }}
        >
          우와! 모달이다!
        </Modal>
      )}
    </>
  );
}

portal 내부에서 발생한 onClick={() => setModalClose(false)}이벤트는 React 트리에 포함된 상위로 전파된다.
즉, id="root"의 App 컴포넌트가 id="modal-root"의 Portal에서 Modal컴포넌트의 onClick={() => setModalClose(false)} 이벤트를 포착하는것 같다.

재조정(Reconciliation)

React에서 효율적인 렌더링을 위해 어떤 비교 알고리즘을 선택했는지 소개한다.

O(n)

리액트는 두 가지 가정을 기반하여 O(n) 복잡도의 휴리스틱 알고리즘을 구현했다.
1. 서로 다른 타입의 두 엘리먼트는 서로 다른 트리를 만들어낸다.
2. 개발자가 key props을 통해, 여러 렌더링 사이에서 어떤 자식 엘리먼트가 변경되지 않아야 할지 표시해 줄 수 있다.

비교 알고리즘

두 개의 트리를 비교할 때, React는 두 엘리먼트의 루트(root) 엘리먼트부터 비교한ㄷ. 이후의 동작은 루트 엘리먼트의 타입에 따라 달라진다.

엘리먼트의 타입이 다른 경우

두 루트 엘리먼트의 타입이 다르면, React는 이전 트리를 버리고 완전히 새로운 트리를 구축한다.
<a> -> <img>로, <Article> -> <Comment>로 바뀌는 것 모두 트리 전체를 재구축하는 경우이다.

<div>
  <Counter />
</div>

<span>
  <Counter />
</span>

이때 Counter는 사라지고, 새로 다시 span을 부모로 둔 Counter가 마운트 된다.
트리를 버릴 때는 이전 DOM 노드들은 모두 파괴된다. 컴포넌트 인스턴스는 componentWillUnmount()가 실행된다. 새로운 트리가 만들어질 때, 새로운 DOM 노드들이 DOM에 삽입된다. 그에 따라 컴포넌트 인스턴스는 UNSAFE_componentWillMount()가 실행되고 componentDidMount()가 이어서 실행된다. 이전 트리와 연관된 모든 state는 사라진다.

주의! UNSAFE_componentWillMount()는 새로운 코드작성 시 피해야 한다.

DOM 엘리먼트의 타입이 같은 경우

같은 타입의 두 React DOM 엘리먼트를 비교할 때, React는 두 엘리먼트의 속성을 확인하여, 동일한 내역은 유지하고 변경된 속성들만 갱신한다.
style이 갱신될 때, React는 변경된 속성만을 갱신한다.

<div style={{color: 'red', fontWeight: 'bold'}} />

<div style={{color: 'green', fontWeight: 'bold'}} />

fontWeight는 수정하지 않고 color 속성 만을 수정한다.
DOM 노드의 처리가 끝나면, React는 이어서 해당 노드의 자식들을 재귀적으로 처리한다.

같은 타입의 컴포넌트 엘리먼트

컴포넌트가 업데이트 되면 인스턴스는 동일하게 유지되어 렌더링 간 state가 유지된다. React는 새로운 엘리먼트의 내용을 반영하기 위해 현재 컴포넌트 인스턴스의 props를 갱신한다. 이때 해당 인스턴스의 UNSAFE_componentWillReceiveProps(), UNSAFE_componentWillUpdate(), componentDidUpdate를 호출한다.

주의! UNSAFE_componentWillUpdate() 와 UNSAFE_componentWillReceiveProps()는 새로운 코드작성 시 피해야 한다.

자식에 대한 재귀적 처리

DOM 노드의 자식들을 재귀적으로 처리할 때, React는 기본적으로 동시에 두 리스트를 순회하고 차이점이 있으면 변경을 생성한다.

<ul>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

<ul>
  <li>Connecticut</li>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

위와 같은 경우 <li>Duke</li> <li>Villanova</li> 종속 트리를 그대로 유지하는 대신 모든 자식을 변경해 버린다. 이미 존재하는 요소지만 말이다.
여기서 key가 나온다. 이러한 비효율을 해결하기 위해 React는 key속성을 지원한다. 자식들이 key를 가진다면, React는 key를 통해 기존 트리와 이후 트리의 자식들이 일치하는지 확인한다!! 이제는 key를 확인해서 엘리먼트를 다시 그리는게 아닌 이동만 하면 된다고 파악할 수 있다.

key는 오로지 형제 사이에서만 유일하면 된다! 다만 배열의 index를 사용하는 것은 지양하자. 항목들이 재배열되는 경우는 비효율적이다. 만약 항목의 순서가 바뀐다면 key또한 바뀌기 때문이다.

휴.. 오늘은 여기까지 읽자.. 공식문서를 읽고 정리를 하던중 면접을 보자고 연락이 왔다. 면접준비도 해야한다. 힘내야지.

profile
그냥 개인적으로 공부한 글들에 불과

0개의 댓글