React 공식문서 _ 02.Adding Interactivity

yesme·2023년 11월 27일
0
post-thumbnail

중간중간 들어가는 🤔 표시는 공식문서에는 없지만 더 알면 좋을 것 같아 내가 정리해본 내용,
📄 는 공식문서임을 의미한다.
바쁘신 분들은 🌟 은 꼭 읽어보시길 추천드린다.

Responding to Events


이벤트 전파 (event bubbling)

  • 위 사진에서 버튼을 선택하면 어떻게 될까?
    정답은 버튼 onClick → div onClick 순서로, children > parent로 이벤트가 진행된다. 이를 이벤트 전파(=이벤트 버블링)라고한다.
  • 이벤트 핸들러는 인자로 event object를 받는데, 이 안에 이벤트에 관한 정보들이 포함되어 있다.
  • 이벤트 전파는 위 event object에 e.stopPropagation()을 호출해 중지할 수 있다.
  • React에서는 이벤트 전파가 많아질수록 코드의 복잡도가 증가하므로, 대안으로 직접 handler를 전달하는 것을 권장하고 있다.

📑 브라우저는 특정 화면 요소에서 이벤트가 발생했을 때 그 이벤트를 최상위에 있는 화면 요소까지 전파시킨다.
버블링(bubbling)은, 이런 브라우저의 이벤트 전파 방식으로 인해 발생하는 특징이다.


event capturing 🧐

드물지만 stop propagation을 호출한 경우에도, children에 대한 이벤트를 잡아야하는 경우가 있다. (ex - 화면내의 모든 click event에 log를 걸어야하는 경우) 이때는 Capture라는 이벤트를 추가하면 된다.

이벤트 캡쳐(Event Capture)는 버블링과 반대로, parent > children로 이벤트가 전파되는 방식이다.

기본 동작 막기 (preventing default behavior)

  • e.stopPropagation() 이벤트 전파(bubbling) 막기
  • e.preventDefault() 몇몇 이벤트에 대해 브라우저가 기본적으로 가진 동작을 막기(ex- 위 예시 form onSumit시 page reload)

State: A Component’s Memory


지역변수로는 충분하지 않다.

위 예시에서 handleClick 이벤트는 지역 변수, index를 업데이트 하는데, 두가지 요인으로 변화가 보이지 않는것을 확인할 수 있다.

  1. 지역변수는 렌더링 간에 유지되지 않는다. React는 이 컴포넌트를 두 번째로 렌더링할 때도, 다시 처음부터 렌더링한다. 따라서, 지역변수의 변화를 고려하지 않는다. (index=0이 유지)
  2. 지역변수를 변경하는 것은 렌더링을 트리거하지 않는다. 리액트는 새로운 데이터로 컴포넌트를 다시 렌더링 해야 한다는 것을 인지하지 못한다.

새로운 데이터로 업데이트 하기 위해서는, 아래 두가지 조건이 필요하다.

  1. Retain 렌더링 사이에 데이터 유지
  2. Trigger 리액트가 새로운 데이터로 컴포넌트 렌더링할 수 있는 트리거 (re-rendering)

useState Hook은 아래 2개를 제공한다.

  1. 렌더 사이에 데이터를 유지하는 상태 변수(state variable)
  2. 변수를 변경하고 다시 컴포넌트를 렌더링하는 상태 setter 함수(state setter function)

📑 use로 시작하는 Hook은 컴포넌트의 top level이나 customHook을 통해서만 호출 가능합니다.
Hook을 조건문이나, 반복문, 함수 내에서 호출할 수 없습니다.


useState 해부해보기

  1. 컴포넌트가 처음으로 렌더된다. 초기값으로 0을 index에 넘겨줬기 때문에, [0, setIndex]를 반환. 리액트는 0을 가장 최근 상태값으로 저장한다.
  2. state를 업데이트한다. 버튼을 클릭하면, setIndex(index + 1)을 호출하고, 현재 index는 0이므로 최종적으로 setIndex(1)을 호출하게된다. setter functionindex의 값을 1로 저장하고, 다른 렌더를 트리거한다.
  3. 컴포넌트가 두번째 렌더된다. 리액트는 useState(0)을 확인하지만, index1로 바꾼것을 기억하고 있으므로 [1, setIndex]를 반환한다.
  4. 위 과정을 반복한다.

🤔 진짜 React Hook 파일은 어떻게 구성되어 있을까?

  1. useState & resolveDispatcher : initialState과 함께 ReactCurrentDispatcher.current 호출 → 클로저를 활용해 함수 외부값에 접근


  2. ReactCurrentDispatcher : 전역으로 선언된 객체 프로퍼티


  3. dispatcher.useState 내부 함수
    1. 실행할 hook을 가져온 뒤, 저장되어 있는 state(state variable)을 불러온다. 없으면 initialState를 가져온다.
    2. 함수가 호출되면 로그 정보를 hookLog 배열에 순서대로 추가한다. 이때 현재 값(state), primitive, 호출된 위치의 스택정보(stackError) 도 함께 저장된다.
    3. setter function 호출 시, hookLog 스택을 돌며 렌더를 트리거하고 값을 업데이트 한다
      (→ 이 내용부터는 코드가 너무 복잡해져서 다른 글로 작성해보는게 좋을듯하다)

React가 어떤 상태를 반환할지 어떻게 알 수 있을까? 🌟

useState를 호출할 때, 자신이 참조하는 state에는 아무 정보가 없다는 것을 확인할 수 있다.

위 예시로 살펴보자면 index - 0, showMore - false로 매칭되는데 useState를 호출할 때는 아무런 “identifier”가 없다.

다시 말해, 0이 index로 매칭되고 showMore가 어떻게 false로 매칭될까?
따로 key를 선언한 것도 아니고 그냥 useState를 두 개 선언했을 뿐인데!

그럼 state마다 어떤 value를 반환할지 도대체 어떻게 알 수 있는걸까?

React의 Hooks은 동일한 컴포넌트를 렌더링할 때마다 안정적인 호출 순서에 의존한다. 위 내용에서 “Hook은 최상위 수준에서만 호출가능”하다고 했었는데, 이 특징으로 인해 Hook은 매번 같은 순서로 호출되게 된다. (추가적으로 linter plugin이 실수를 잡아낸다.)

내부적으로 React는 모든 컴포넌트에 대한 state pair 배열을 보유하고 있다.
이 배열은 렌더 전부터 0으로 초기화 된 현재 pair 인덱스를 유지하는데, 이를 사용해 useState를 호출할 때마다, 다음 state pair를 제공하고 인덱스를 증가시킨다.
이 내용에 대한 자세한 내용은 React hook: not magic, just arrays 에서 확인할 수 있다.

아래는 실제 React에서 사용되는 코드는 아니지만, useState의 동작 원리에 맞춰 설계한 코드이다. (공식문서에 나와있는 코드 그대로)

let componentHooks = [];
let currentHookIndex = 0;

function useState(initialState) {
  let pair = componentHooks[currentHookIndex];
  if (pair) {
		// 첫번째 렌더가 아니므로, state pair가 이미 존재한다. 
		// 이 pair를 반환하고 다음 훅이 호출되면 사용한다.
    currentHookIndex++;
    return pair;
  }

	// 첫번째 렌더링시 생성 및 저장되는 pair (초기값, setState)
  pair = [initialState, setState];

  function setState(nextState) {
		// state를 변경하면, pair[0] - value에 새로운 값을 할당하고 DOM을 업데이트한다
    pair[0] = nextState;
    updateDOM();
  }

	// pair를 저장하고, 다음으로 호출될 Hook 준비 (index+1)
  componentHooks[currentHookIndex] = pair;
  currentHookIndex++;
  return pair;
}

function Gallery() {
  // useState를 사용해 pair값을 받을 수 있다
  const [index, setIndex] = useState(0);
  const [showMore, setShowMore] = useState(false);

  function handleNextClick() {
    setIndex(index + 1);
  }

  function handleMoreClick() {
    setShowMore(!showMore);
  }

  let sculpture = sculptureList[index];

  return {
    onNextClick: handleNextClick,
    ...
  };
}

function updateDOM() {
	// 렌더링 전, currentHookIndex 초기화 (저장/업데이트 된 pair 불러옴)
  currentHookIndex = 0;
  let output = Gallery();

  // Update the DOM to match the output.
  // This is the part React does for you.
  nextButton.onclick = output.onNextClick;
	...
}

let nextButton = document.getElementById('nextButton');
...

updateDOM();

State는 독립적이고, private하다.

state는 화면 컴포넌트 구성요소에 local로 존재한다. 동일한 컴포넌트를 2번 렌더링하면, 각 복사본(컴포넌트)은 완전히 독립적인 state를 가진다.

<Gallery /> 내부의 독립적인 state들은 서로 영향을 주지 않는다.

내부의 독립적인 state들은 서로 영향을 주지 않는다.

이 점이 모듈 상단에서 선언할 수 있는 일반 변수(지역변수)와 다른 점이다. State는 화면의 특정 위치에 local로 존재한다. 위 예제에서 두 개의 <Gallery /> 구성요소를 렌더링했으므로, 각 state가 별도로 저장된다.

또한 페이지 컴포넌트는 <Gallery /> state에 관해 아무것도 알지 못한다. props와 달리 state는 이를 선언하는 컴포넌트(Page)에도 완전히 비밀로 유지된다. parent 컴포넌트에서 이를 바꿀 수 없다.

<Gallery /> 상태를 동일하게 유지하고 싶다면, 각 컴포넌트에 있는 state를 제거해 parent에서 이를 관리해야한다.


Render and Commit


Step 1: Trigger a render

컴포넌트가 렌더되는 2가지 원인은 아래와 같다.

  1. 초기 렌더링(Initial render)

    렌더할 DOM 노드에 [createRoot](https://react.dev/reference/react-dom/client/createRoot)render method를 함께 호출하면 된다.

  2. 컴포넌트(혹은 부모 컴포넌트)의 state가 업데이트

    초기 렌더링 후에, set function으로 state를 업데이트 할 수 있다.

    state를 업데이트하면 렌더링이 자동으로 대기열에 추가된다.

Step 2: React renders your component

📄 ”렌더링” 이란 리액트가 컴포넌트를 호출하는 것을 의미한다.

  1. 초기 렌더링

    DOM node를 생성

  2. 리렌더링

    이전 렌더와 비교해 변경된 내용이 있는지 계산

📄 렌더링은 순수 함수여야한다.

  • 동일한 input, 동일한 output
  • 현재 주어진 일만 처리1

Step 3: React commits changes to the DOM

  1. 초기 렌더링

    [appendChild()](https://developer.mozilla.org/ko/docs/Web/API/Node/appendChild) DOM API를 사용해 DOM 노드를 생성

  2. 리렌더링

    • 최신 렌더링 결과물과 일치하도록, 렌더링 중에 계산된 최소한의 필수 작업만 적용
    • 두 렌더사이에 차이가 있을때만 DOM을 업데이트한다.

에필로그: Browser paint

렌더링이 끝나고 리액트가 DOM을 업데이트하면, 브라우저가 화면을 repaint 한다.

(이 프로세스 전체를 “브라우저 렌더링” 이라고 하지만, 문서 내에서는 혼동을 제거하기 위해 “페인팅” 이라고 명명)


State as a Snapshot


snapshot을 찍는 Rendering

  • 렌더링 후 반환되는 JSX는 그 시간에 찍힌 UI 스냅샷 같은 함수이다.

  • 이때 찍힌 스냅샷에는 이벤트 핸들러, props, 지역 변수등이 모두 포함되어 있다.

  • 컴포넌트를 re-render 하면 아래 과정이 실행된다.

    1. 리액트가 연결된 함수를 재호출
    2. 함수가 새로운 JSX 스냅샷을 반환
    3. 반환한 스냅샷을 스크린에 업데이트
  • state는 컴포넌트 메모리로서 함수 반환과 함께 삭제되지 않는다.
    컴포넌트는 렌더링 한 state 값을 사용해 props, 이벤트 핸들러를 포함한 JSX 스냅샷을 계산한다.

    • state의 변경은 오직 다음 렌더링에서만 발생하게 된다.
    • 위 예제에서 렌더 전 number 값을 이용하므로 동일한 setNumber 함수가 3번 실행된다. 따라서 버튼을 눌러도 1만 증가한다.

snapshot과 시간의 흐름 🌟

위 예제 코드를 실행하면 alert에 무엇이 뜰까?
정답은….. “0”이 뜬다!

(위에서 다 나온 얘기지만) setTimeout내의 number0으로 스냅샷이 저장되어 있으므로, 렌더링 전 값인 0을 반환하게 된다.

state 변수값은 이벤트 핸들러가 비동기적이여도 렌더 전에는 절대 변경되지 않는다.
React는 한 렌더의 이벤트 핸들러 내에서 state 값을 “고정”으로 유지한다.


Queueing a Series of State Updates


배치 상태 업데이트

이전 예제에서, 각 렌더마다 state 상태는 고정이기 때문에 결국 setNumber(0 + 1);이 3번 호출된다고 했었는데, 숨겨진 사실이 하나 더 있다.

리액트는 state 업데이트를 처리하기 전에, 모든 이벤트 핸들러 코드가 실행되는 것을 기다린다. 이것이 모든 setNumber() 호출후에 리렌더링 되는 이유이다.

이를 batching 이라 하며, 다발적인 state 변화를 많은 re-render 없이 업데이트할 수 있게 해주므로 앱을 더욱 빠르게 만들어준다.

또한 모든 이벤트 핸들러가 실행 및 완료되고 나서야 UI를 업데이트하는 특징이 있다.

다음 렌더링 전, 동일한 상태 업데이트

n => n + 1update function이라 한다. 이를 사용하면,

  1. React가 이벤트 핸들러의 다른 모든 코드가 실행된 후에 함수가 처리되도록 대기열(queue)에 추가
  2. 다음 렌더링 동안 대기열을 거쳐 최종 업데이트된 상태를 제공 → 이때 n은, 이전 update function의 return 값을 의미한다.

state 업데이트 및 교체

update function 외에 다른값이 들어오면 이미 대기열에 있는 항목은 무시되고 replace 함수가 대기열에 추가된다.

update function은 렌더링 중에 실행되므로, 순수 함수여야하며 결과를 반환해야 한다.


Updating Objects in State


mutation 이란?

object state 내부 value를 스스로 변경이 가능한 위와 같은 형태를 mutation이라고 한다.

하지만 object가 변경 가능 하더라도 리액트 내에서는 불변성을 지켜줘야하므로, 값을 넣는 대신 변경해야한다.

Immer를 사용해 로직 업데이트하기

중첩된 객체가 많아서 코드가 복잡해졌을때, immer를 사용하면 불변성을 신경쓰지 않으면서 가독성있는 코드를 작성할 수 있다.

mutating 상태가 React에서 권장되지 않는 이유

  1. 디버그: mutate를 사용하면 console.log에 변화가 감지되지 않으므로, 렌더링 사이에 정확한 state 값 확인 불가능
  2. 최적화: 현재 리액트 최적화 전략은 props나 state가 동일한지 판단 후, 작업을 건너뛰는 것에 의존한다. mutate 하지 않아야, 이 변경사항을 확인하는 과정이 빠르다.
  3. 새로운 기능: 리액트는 스냅샷처럼 처리되는 상태에 의존하고 있으므로, mutate 하면 새로운 기능을 사용하지 못할 수 있다.

🤔 이외에도 다수 있는데.. 그냥 사용하지 말자.


Updating Arrays in State


mutate 없이 배열 업데이트하기

  • 대안으로 Immer를 사용할 수도 있다.
  • slice, splice가 네이밍이 유사한데 리액트에서는 slice만 사용하자!

나머지 내용은, 아는내용 + object 에 있는 내용이라 패스

profile
코드 깎는 개발자..

0개의 댓글

관련 채용 정보