Level 2: react-lotto 학습 로그

동동·2021년 4월 21일
0

0. PR 링크

  1. repository: bigsaigon333/react-lotto

  2. step 1: [1단계 - 행운의 로또미션] 동동(김동희) 미션 제출합니다. #24

  3. step 2: [2단계 - 행운의 로또미션] 동동(김동희) 미션 제출합니다. #54

1. What I Learned

1-1. React는 무엇인가요?

Front-End의 가장 중요한 부분은 상태관리로서, 상태(Model)의 변화에 따라 View를 최선의 타이밍에, 최소한의 조작으로 변경시키는 것입니다. React는 이러한 상태 관리를 편하게 해주어서 매우 인기가 많은데요, React는 UI를 만들기 위한, 즉 웹앱의 View Layer를 만들기 위한 JavaScript 라이브러리입니다.

React는 기본적으로 React Node 트리를 관리합니다. 이 트리는 노드들의 diff 계산을 효율적으로 할 수 있습니다. React는 효율적으로 React Node Tree(Virtual DOM)를 재구성하고, 실제로 발생한 변화만을 DOM에 반영합니다. React Node Tree는 Plain Old Javascript Object입니다. JSX는 단순히 React.createElement의 Syntatic Sugar입니다. JSX는 단순히 React.createElement의 syntatic sugar입니다. babel으로 transpile 해보면 React.createElement는 단순히 plain old javascript object를 반환합니다. 즉, JSX가 parsing되어 모든 React.createElement가 해결되면, 매우 거대한 nested object를 얻게 됩니다. (바로 Tree!)

구성된 UI의 트리를 render하기 위해서는 renderer가 필요합니다. renderer는 OS 또는 플랫폼으로부터 UI를 render하기 위한 지원이 필요하므로 OS 또는 플랫폼에 종속적이지만, React는 그러하지 않습니다.
예를 들어 웹 앱에서는 ReactDOM이, 모바일에서는 react-native가 renderer에 해당합니다. ReactDOM은 React Node Tree를 실제로 render하는 renderer입니다.

https://reactjs.org/
https://www.freecodecamp.org/news/react-under-the-hood/

1-2. React Reconciliation

React는 virtual DOM 의 어떤 변화가 생기면 이를 비교하여 real DOM에 반영합니다. 하나의 트리를 다른 트리로 변환하기 위한 여러 알고리즘이 있지만, 최첨단의 알고리즘도 n개의 엘리먼트가 있는 트리에 대해 O(n^3)의 복잡도를 가집니다. 이는 매우 비싼 연산이므로, React는 아래의 두 가지 가정에 기반하여 O(n) 복잡도의 휴리스틱 알고리즘을 구현하였습니다.

  1. 서로 다른 타입의 두 엘리먼트는 서로 다른 트리를 만들어낸다.
  2. 개발자가 key prop을 통해, 여러 렌더링 사이에서 어떤 자식 엘리먼트가 변경되지 않아야 할지 표시해 줄 수 있다.

비교 알고리즘 (Diffing Algorithm)

두 개의 트리를 비교할 때 React는 두 엘리먼트의 루트(root) 엘리먼트부터 비교합니다.

  1. 엘리먼트의 타입이 다른 경우: 이전 트리를 버리고 완전히 새로운 트리를 구축
React.createElement(
  type,
  [props],
  [...children]
)
// Babel Transpile 전
const Button = () => (<button type="button">버튼</button>)

const App = () => (
	<div>
		<h1>Hi!</h1>
  	<Button />
  </div>
);

// Babel Transpile 후
"use strict";

const Button = () => 
/*#__PURE__*/React.createElement("button", {type: "button"}, "\uBC84\uD2BC");

const App = () => 
/*#__PURE__*/React.createElement("div", null, 
	/*#__PURE__*/React.createElement("h1", null, "Hi!"), 
	/*#__PURE__*/React.createElement(Button, null)
);
	

예를 들어 <a> → <img>, <Button> → <Article> 로 바뀌는 경우 모두 트리 전체를 재구축합니다.

트리를 버릴 때 이전 DOM 노드들은 모두 파괴됩니다. 컴포넌트 인스턴스는 componentWillUnmount()가 실행됩니다. 새로운 트리가 만들어질 때, 새로운 DOM 노드들이 DOM에 삽입됩니다. 그에 따라 컴포넌트 인스턴스는componentDidMount()가 이어서 실행됩니다. 이전 트리와 연관된 모든 state는 사라집니다. 루트 엘리먼트 아래의 모든 컴포넌트도 언마운트되고 그 state도 사라집니다.

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

  • DOM 엘리먼트의 타입이 같은 경우: 같은 타입의 두 React DOM 엘리먼트를 비교할 때, React는 두 엘리먼트의 속성을 확인하여, 동일한 내역은 유지하고 변경된 속성들만 갱신합니다. DOM 노드의 처리가 끝나면, React는 이어서 해당 노드의 자식들을 재귀적으로 처리합니다.
  • 같은 타입의 컴포넌트 엘리먼트: 컴포넌트가 갱신되면 인스턴스는 동일하게 유지되어 렌더링 간 state가 유지됩니다. React는 새로운 엘리먼트의 내용을 반영하기 위해 현재 컴포넌트 인스턴스의 props를 갱신합니다. 이때 해당 인스턴스의 componentDidUpdate를 호출합니다. 다음으로 render() 메서드가 호출되고 비교 알고리즘이 이전 결과와 새로운 결과를 재귀적으로 처리합니다.

재귀적으로 처리할 때

기존의 트리와 현재 트리의 차이를 비교할 때, 예를 들어, list의 첫번째에 엘리먼트가 추가된 경우, React는 기존 트리와 현재 트리의 자식을 동시에 순회하여 차이가 있는 경우 변화를 생성한다.

<!-- 기존 트리 -->
<ul>
  <li>first</li>
  <li>second</li>
</ul>

<!-- 현재 트리 -->
<ul>
  <li>new</li>    <!-- new만 추가되었으나 모든 자식을 변경한다!-->
  <li>first</li>
  <li>second</li>
</ul>

자식들이 key를 가지고 있다면, React는 key를 통해 기존 트리와 이후 트리의 자식들이 일치하는지 확인합니다.

<!-- 기존 트리 -->
<ul>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>

<!-- 현재 트리 -->
<ul>
  <li key="2014">Connecticut</li>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>

Component를 강제로 unmount하는 방법

이를 역으로 활용한다면, 동일한 타입의 엘리먼트도 key 값을 다르게 주어 강제로 unmount → mount 시킬 수 있습니다. 언마운트되면 관련 state도 사라지므로, state를 초기화하는 방법으로 사용할 수 있습니다.

const App = () => {
  const [key, setKey] = useState(0);
  const handleReset = () => {
    setKey(key + 1);
  };

  return <Lotto key={key} onReset={handleReset} />;
};

ReactDOM 의 unmountComponentAtNode 메서드 활용하는 방법도 있습니다.

const handleResetClick = () => {
  const $root = document.querySelector("#root");

  ReactDOM.unmountComponentAtNode($root);
  ReactDOM.render(<App />, $root);
}

다만, ReactDOM 을 건드리는 곳이 index.jsx 외에 또 있다면 추후에 유지보수시에 굉장히 어려울 것 같습니다. ReactDOM.render(<App />, $root) 한 후, render는 온전히 ReactDOM에 맡기는 것이 React가 가장 잘 일하도록 하는 방법인 것 같습니다.

https://ko.reactjs.org/docs/reconciliation.html

1-3. 함수 컴포넌트의 LifeCycle Method

함수 컴포넌트는 클래스 컴포넌트와는 달리 componentDidMount, componentDidUpdate, componentWillUnmount 등의 LifeCycle Method 가 없습니다. 단, useEffect hook을 사용하여 LifeCycle Method를 흉내낼 수는 있습니다.

클래스 컴포넌트는 React에 의해 인스턴스화되어, Mount → Unmount 까지의 흐름이 제어되지만, 함수 컴포넌트는 클래스 컴포넌트와는 달리 인스턴스화되지 않고, 호출되어 실행 후 return 값을 반환하고 종료되기 때문에 LifeCycle이 없기 때문입니다.

따라서, 함수 컴포넌트에서는 클래스 컴포넌트와 같이 컴포넌트의 LifeCycle 을 고려하여 구현하는 것은 바람직하지 않습니다. useEffect Hook을 사용하여 LifeCycle Method를 흉내내는 경우에도, 컴포넌트의 mount, update 여부에 따라서 특정 로직을 실행시키는 것이 아니라, dependencies의 값이 변경된 경우에 특정 로직을 실행시키도록 하여 React의 DOM 반영 시점을 염두에 두지 않는 것이 중요하겠습니다. 다만, 최초 Mount되었을 때에만 특정 로직을 실행하겠다(componentDidMount 타이밍에 무엇을 하겠다)는 로직은 꽤나 빈번하게 사용됩니다. 이러한 로직은 별도의 hook을 사용하면 편리하게 할 수 있습니다.

const useEffectOnce = (fn) => {
	useEffect(fn, []);
}
profile
작은 실패, 빠른 피드백, 다시 시도

0개의 댓글