본문 바로가기

나야, 리액트 스터디

[week3]상호작용성 더하기 - 이벤트에 응답하기, State: 컴포넌트의 기억 저장소, 랜더링 그리고 커밋, 스냅샷으로서의 state

더보기
안녕하세요! 웹파트 OB 김건휘입니다. 이번 시간에는 리액트 공식문서 - 상호작용성 더하기 - (이벤트에 응답하기, State: 컴포넌트의 기억 저장소, 랜더링 그리고 커밋, 스냅샷으로서의 state)내용 중에서 궁금한 부분 딥다이브 및 요약에 대한 아티클을 작성해보겠습니다.

 

이벤트에 응답하기

"이벤트에 응답하기"의 내용 중 e.preventDefault()에 대한 설명을 위해, <form>태그의 기본 동작에 대한 설명이 있었다. 사실 이전에도 <button>, <form>과 같은 대표적인 HTML 요소들의 기본동작은 어느정도 알고 있었는데, 다양한 HTML 요소들의 기본동작에 대해서 알아보고 싶어서 공부하게 되었다.

 

📌HTML 요소들의 기본동작

 

  • <a> (링크 태그):
    • href 속성에 지정된 URL로 이동하는 것이 기본 동작이다. 클릭 시 페이지를 다른 곳으로 이동시키거나 새 탭에서 열리도록 설정할 수 있다.
  • <button>:
    • <form> 안에 있는 경우, 클릭 시 폼을 제출하고 페이지를 리로드하는 것이 기본 동작이다.
    • type="button"으로 지정하면 폼 제출 없이 클릭 이벤트만 발생한다. 즉, <form>안에 <button>이 있는 경우 type을 명시해주지 않아도 <button type="submit">이 기본으로 설정되고, <form> 밖에 있을 때 type="button"으로 처리된다.
  • <input type="text">, <textarea> (텍스트 입력 필드):
    • 사용자가 텍스트를 입력할 수 있게 하고, 엔터 키를 누르면 폼을 제출하는 것이 기본 동작이다.
  • <input type="checkbox">, <input type="radio"> (체크박스 및 라디오 버튼):
    • 클릭 시 체크 상태를 토글하거나, 같은 name을 가진 라디오 버튼 중 하나를 선택하는 것이 기본 동작이다.
  • <select> (드롭다운 리스트):
    • 사용자가 목록에서 항목을 선택할 수 있도록 하고, 선택된 항목을 표시하는 것이 기본 동작이다.
  • <img> (이미지 태그):
    • src 속성에 지정된 이미지를 페이지에 표시하는 것이 기본 동작이다. 이미지가 없는 경우 alt 텍스트를 표시한다.
  • <label>:
    • for 속성에 해당하는 id를 가진 폼 요소에 포커스를 맞추는 것이 기본 동작이다. 폼 요소에 레이블을 제공하며, 레이블을 클릭해도 해당 폼 요소가 선택된다.
  • <video> 및 <audio>:
    • autoplay 속성이 없을 경우 재생 컨트롤러를 표시하고 사용자가 재생 버튼을 클릭하여 재생을 시작할 수 있도록 하는 것이 기본 동작이다.

e.preventDefault()를 사용하면 HTML 요소들의 기본동작을 막을 수 있다!

 

🧐그런데 e.preventDefault()를 남용하지말라고??

🚨기본 동작 막기를 너무 남용하지 마세요.
기본 동작을 막는 자바스크립트 코드를 추가하면 제약 없이 요소의 동작을 원하는 대로 바꿀 수 있습니다. 링크 <a>를 버튼처럼 만들 수 있고, 버튼 <button>을 다른 URL로 이동시켜주는 링크처럼 동작하게 할 수도 있습니다.하지만 HTML 요소의 의미를 지키면서 동작을 바꿔야 합니다. <a>는 페이지를 돌아다니는 동작을 해야 하지 버튼처럼 동작해선 안 됩니다.이렇게 요소가 가진 의미를 해치지 않으면서 코드를 작성하면 '좋은 코드’가 될 뿐만 아니라 접근성 측면에서도 도움이 됩니다.<a>와 기본동작 막기를 조합한 코드를 구상할 때 주의할 것이 있습니다. 사용자는 브라우저 기본 동작을 사용해 마우스 우클릭 등의 방법으로 새 창에서 링크를 열 수 있습니다. 이 기능은 인기가 많죠. 하지만 자바스크립트로 버튼을 조작해 링크처럼 동작하게 만들고 CSS를 이용해 버튼을 링크처럼 꾸미더라도 브라우저에서 제공하는 <a> 관련 기능은 버튼에선 작동하지 않습니다.

=> 위의 내용은 모던 Javascript에 작성되어 있는 내용이다. 해당 내용을 요약해보면 구현하는데 있어서 필요한 동작은 HTML에서 제공하는 태그 중 적합한 태그를 사용해야하며, e.preventDefault()를 사용하여 기본동작을 막는 것은 신중해야한다!

 

렌더링 그리고 커밋

📌렌더링이란?
 리액트에서 렌더링(rendering)이란, 컴포넌트의 내용을 화면에 표시하거나 업데이트하는 과정을 의미한다.

 

- 초기 렌더링: 구성 요소가 화면에 처음 나타날 때 발생
- 리렌더링: 이미 화면에 있는 구성 요소의 두 번째 및 연속 렌더링

📌렌더링 프로세스
리액트에서는 크게 아래의 3단계를 거쳐 렌더링 과정이 이루어진다.
1. 렌더링 트리거
2. 컴포넌트 렌더링
3. DOM에 커밋

 



리액트 공식문서에서는 리액트를 웨이터에 비유한다. 1.렌더링 트리거 (setState와 같은 상태 변경)는 손님의 주문을 주방으로 전달. 2. 컴포넌트 렌더링: 주방에서 요리를 준비. 3.DOM에 변경사항을 커밋 : 테이블에 전달.
=> 위의 과정이 이루어질 수 있도록 리액트는 중간에서 웨이터와 같이 요청, 준비, 전달의 역할을 한다고 할 수 있다.

📌1단계: 렌더링 트리거
컴포넌트 렌더링이 일어나는 데에는 두 가지 이유가 있다.

- 컴포넌트의 초기 렌더링인 경우
- 컴포넌트의 state가 업데이트된 경우

✅초기 렌더링
컴포넌트가 처음으로 화면에 나타날 때, 리액트는 컴포넌트 트리를 바탕으로 가상 DOM을 생성하고, 이를 실제 DOM에 반영하여 화면에 처음으로 렌더링하는 과정.

✅State 업데이트 시 리렌더링
컴포넌트의 state가 변경될 때마다, 리액트는 해당 컴포넌트를 다시 렌더링하여 변경된 내용을 가상 DOM에 반영하고, 변경된 부분만 실제 DOM에 업데이트하는 과정.

📌2단계: React 컴포넌트 렌더링
렌더링을 트리거한 후 React는 컴포넌트를 호출하여 화면에 표시할 내용을 파악한다. “렌더링”은 React에서 컴포넌트를 호출하는 것이다.

- 초기 렌더링에서 React는 루트 컴포넌트를 호출한다.
- 이후 렌더링에서 React는 state 업데이트가 일어나 렌더링을 트리거한 컴포넌트를 호출한다.

 

React는 기본적으로 부모 컴포넌트가 렌더링되면, 그 안에 있는 모든 자식 컴포넌트를 재귀적으로 렌더링한다. => 일반적으로 컴포넌트가 렌더링되면 그 안에 있는 모든 컴포넌트 역시 렌더링 된다.

또 주목해야할 점은 일반적인 렌더링 과정에서, React는 "Props가 변경되었는지 여부"는 신경쓰지 않는다. 그저 부모 컴포넌트가 렌더링되었기 때문에 자식 컴포넌트도 무조건 렌더링하는 것이다. 

 

 ✅예시코드

export default function Clock({ time }) {
  return (
    <>
      <h1>{time}</h1>
      <input />
    </>
  );
}

위의 코드와 같이 time이 +1초씩 증가하는 코드가 있다고 생각해보자. 우리들은 보통 props로 전달받은 time이 변경되기 때문에 자식 컴포넌트인 Clock 컴포넌트에서 렌더링 된다고 생각할 수 있지만, 정확하게 말하면  부모 컴포넌트에서 time 값이 변경되면서 부모가 다시 렌더링되고, 그로 인해 자식 컴포넌트인 Clock 컴포넌트도 다시 렌더링된다는 관점으로 해석해야 적절하다는 것이다.

🧐왜 그렇게 동작할까?
리액트는 컴포넌트 계층을 리렌더링하는 기본 동작을 통해 데이터 흐름을 유지하고 단방향 데이터 바인딩을 보장할 수 있다. 부모가 업데이트되면 자식들도 자동으로 갱신되므로, 데이터가 의도치 않게 오래된 상태로 남아 있는 것을 방지할 수 있다.

리액트는 컴포넌트를 언제 렌더링할지 매우 효율적으로 처리하지만, "부모 렌더링 → 자식 렌더링"의 단순 규칙을 따르는 것이 최적화 측면에서 더 낫다고 판단하였다.

🧐그럼 props 변경 여부와는 전혀 관련이 없는건가?
리액트의 기본 렌더링 방식에서는 자식 컴포넌트가 props가 변경되지 않아도 렌더링되는 것이 맞지만, 특정 경우에 props 변경 여부를 기반으로 최적화를 할 수 있다. 
- memo를 사용하여, 부모가 렌더링되더라도 props가 이전과 동일한 경우 자식 컴포넌트의 렌더링을 방지하는 방식으로 최적화 가능

📌3단계: React가 DOM에 변경사항을 커밋

컴포넌트를 렌더링(호출)한 후 React는 DOM을 수정한다. 
초기 렌더링의 경우 React는 `appendChild()` DOM API를 사용하여 생성한 모든 DOM 노드를 화면에 표시한다.
리렌더링의 경우 React는 필요한 최소한의 작업(렌더링하는 동안 계산된 것)을 적용하여 DOM이 최신 렌더링 출력과 일치하도록 한다.

 React는 렌더링 간에 차이가 있는 경우에만 DOM 노드를 변경한다!

✅예시코드

javascript
export default function Clock({ time }) {
  return (
    <>
      <h1>{time}</h1>
      <input />
    </>
  );
}

위의 예시와 동일한 코드를 가지고 왔다. React가 <h1>의 내용만 새로운 time으로 업데이트하기 때문에 <input>이 JSX에서 이전과 같은 위치로 확인되므로 React는 <input>또는 value를 건드리지 않는다. =>  <input>에 텍스트를 입력하여 value를 업데이트 하지만 컴포넌트가 리렌더링될 때 텍스트가 사라지지 않는다.

스냅샷으로서의 State

State 변수는 읽고 쓸 수 있는 일반 자바스크립트 변수처럼 보일 수 있다. 하지만 state는 스냅샷처럼 동작한다. state 변수를 설정하여도 이미 가지고 있는 state 변수는 변경되지 않고, 대신 리렌더링이 발동된다.

 

해당 내용은 리액트 공식문서의 스냅샷으로서의 State 카테고리에서 첫번째 문단에서 발췌한 것이다.

 state를 설정하면 React에 리렌더링을 요청한다는 것은 알고 있을 것이다. 여기서 요점은 렌더링은 그 시점의 스냅샷을 찍는다는 것이다.
 
📌스냅샷(snapshot)이 뭔데?
스냅샷(Snapshot)이란 "순간"을 포착한 것.
해당 순간의 정보를 기억하고 있는 것이라고 이해하면 편하다.

“렌더링”이란 React가 컴포넌트, 즉 함수를 호출한다는 뜻이다. prop, 이벤트 핸들러, 로컬 변수는 모두 렌더링 당시의 state를 사용해 계산된다.

사실 말로만 들으면 이해가 어려울 수 있기에 예시 코드로 설명을 해보고자 한다.

import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button
        onClick={() => {
          setNumber(number + 1);
          setNumber(number + 1);
          setNumber(number + 1);
        }}>
        +3
      </button>
    </>
  );
}


버튼 클릭 시 한 번에 이전 상태에 3이 더해진게 나타날거라 생각할 수도 있다. 하지만 코드를 실행해보면 1만 더해지는 것을 알 수 있다.

 🧐WHY?
setNumber(number + 1)를 세 번 호출했지만, 이 렌더링에서 이벤트 핸들러에서 number는 항상 0이므로 state를 1로 세 번 설정한다. 이것이 이벤트 핸들러가 완료된 후 React가 컴포넌트 안의 number 를 3이 아닌 1로  다시 렌더링하는 이유이다.

렌더링에서 `number state` 변수는 `0`이므로 이벤트 핸들러는 다음과 같다.

<button onClick={() => {
  setNumber(0 + 1);
  setNumber(0 + 1);
  setNumber(0 + 1);
}}>+3</button>


즉, 랜더링 시점의 number값인 0을 스냅샷으로 찍어서 기억하고 있고, onClick 이벤트가 발생하여서 리렌더링이 발생하게 되면 스냅샷을 찍었던 시점의 상태의 값인 number값의 0을 활용하여 업데이트 해준다는 것이다.

🧐즉시 상태를 반영하게 하려면?

<button
  onClick={() => {
    setNumber((prev) => prev + 1);
    setNumber((prev) => prev + 1);
    setNumber((prev) => prev + 1);
  }}>
  +3
</button>


React의 함수형 업데이트를 사용하면, 각 상태 업데이트에서 이전 상태를 정확히 참조할 수 있다. 이 방법은 상태가 업데이트될 때마다 최신 값을 보장한다.

 

이번시간에 다룬 내용들은 리액트의 동작원리와 랜더링 매커니즘에 대해서 이해할 수 있는 시간이었다. 

 

더보기

🧐궁금증

위의 코드에서 <Gallery /> 컴포넌트를 두 번 렌더링했으므로 state는 별도로 저장되는걸로 알고 있는데, 어떤식으로 별도로 저장되고 관리되는지 메커니즘이 궁금하다!