kimyenac
blog
front-end
2장 리액트 핵심 요소 깊게 살펴보기
2024-07-23

JSX

  • JSX 는 HTML 이나 XML 을 자바스크립트 내부에 표현하는 것이 유일한 목적은 아니다. JSX 내부에 트리 구조로 표현하고 싶은 다양한 것들을 작성해두고, 이 JSX를 트랜스파일 과정을 거쳐 자바스크립트가 이해할 수 있는 코드로 변경하는 것이 JSX 의 목표라고 할 수 있다.
  • JSX 구성 컴포넌트
    • JSXElement : HTML 요소와 비슷한 역할을 하는 가장 기본 요소
    • JSXAttributes : JSXElement에 부여할 수 있는 속성 (필수 아님)
    • JSXChildren : JSXElement의 자식 값을 나타냄 (0개 이상 존재 가능)
    • JSXStrings : 문자열 (개발자가 HTML의 내용을 손쉽게 JSX 로 가져올 수 있도록 의도적으로 설계된 부분)
  • JSX 문법에는 있지만 실제로 리액트에서 사용하지 않는 것들
    • JSXNamespacedName
    • JSXMemberExpression


가상 DOM과 리액트 파이버

  • 가상 DOM 을 통한 DOM 관리가 직접 DOM 을 조작하는 것보다 빠르다고 오해를 하는데, 가상 DOM 의 diffing, 배치 업데이트 과정에 추가적인 리소스 소모가 있을 수 있다. 실제로 직접 DOM을 조작해서 리액트보다 더 빠른 속도를 가진 라이브러리들이 존재함. (svelte 🙃)
    • 가상 DOM 의 이점은 상태 변경에 따라 전체 UI를 새로 그리는 것처럼 개발할 수 있으나 실제 DOM 반영은 일부만 되는 것과, 컴포넌트 트리를 가상 DOM 이란 레이어로 추상화하여 실제로 그리는 렌더러를 바꿀 수 있는 것.
  • 리액트 파이버 : 리액트에서 어떤 부분을 새롭게 렌더링해야 하는지 가상 DOM과 실제 DOM을 비교하는 작업을 하는 자바스크립트 객체로 하는 일은 다음과 같다.
    • 작업을 작은 단위로 분할하고 쪼갠 다음, 우선순위를 매긴다
    • 이러한 작업을 일시 중지하고 나중에 다시 시작할 수 있다
    • 이전에 했던 작업을 다시 재사용하거나 필요하지 않은 경우에는 폐기할 수 있다
  • 리액트 파이버 트리 : 현재의 모습을 담은 current 파이버 트리와 작업중인 상태를 나타내는 workInProgress 트리가 있음. 리액트 파이버의 작업이 끝나면 트리를 가리키는 포인터만 변경해 workInProgress 트리를 current 트리로 바꿔치기 하는데 이 기법을 더블 버퍼링이라고 함. (리액트에서 더블 버퍼링은 커밋 단계에서 수행됨.)
  • 파이버 동작 방식
    • setState 등으로 업데이트가 발생하면 workInProgress 트리를 빌드하여 상태가 변경되는 부분을 반영 (렌더 단계)
    • 이를 리액트 DOM 혹은 React Native 내부의 렌더러 등이 실제 렌더링을 진행 (커밋 단계)
  • 리액트 파이버와 DOM/Native 렌더링은 별개. 따라서 렌더러가 다르다 하더라도 동일한 재조정자를 사용할 수 있음


클래스 컴포넌트와 함수 컴포넌트

  • 리액트에서는 클래스와 함수를 기반한 컴포넌트를 구현할 수 있는데 클래스 컴포넌트의 한계를 극복하여 장점으로 부각되는 게 함수 컴포넌트. 다만 생명주기 메서드 중 함수 컴포넌트로 모사 불가능한 것이 필요할 경우 클래스 컴포넌트가 필요함.
  • 클래스 컴포넌트의 한계 → 함수 컴포넌트의 장점
    • 데이터 흐름을 추적하기 어려움 → this 와 관련된 혼란을 줄이고 hooks api 를 통해 일관된 흐름을 유지
    • 로직 재사용이 어려움 → 커스텀 훅으로 로직 재사용 가능
    • 클래스의 한계로 번들 크기가 증가됨 → 사용하지 않는 함수는 제거해서 불필요한 번들 크기를 줄일 수 있음
    • 인스턴스 내부에 state 를 관리하여 핫 리로딩에 불리함 → 리액트 아키텍처 내부 클로저에 state 저장하여 핫 리로딩에 최적화 되어있음.
  • 클래스 컴포넌트 vs 함수 컴포넌트
    • 클래스 컴포넌트는 props, state 의 값을 항상 this 로부터 가져옴
    • 함수 컴포넌트는 props 를 함수의 인자로 받는데 컴포넌트가 그 값을 변경할 수 없고, 해당 값을 그대로 사용하게 됨


렌더링은 어떻게 일어나는가?

  • 리액트의 렌더링은 리액트 애플리케이션 트리 안에 있는 모든 컴포넌트들이 현재 가지고 있는 props 와 state 의 값을 기반으로 어떻게 UI를 구성하고 이를 바탕으로 어떤 DOM 결과를 브라우저에 제공할 것인지 계산하는 과정
  • 렌더링이 발생하는 시나리오
    • 최초 렌더링: 처음 애플리케이션 진입 시 발생되는 렌더링으로 최초 화면 그리기 정보 제공을 위함
    • 리렌더링: 최초 렌더링 이후로 발생하는 모든 렌더링을 의미하며, state 업데이트 / key props 변경 등의 이유로 발생됨
  • 렌더링 프로세스 : 상태 변경을 감지하고, 변경된 상태를 기반으로 새로운 가상 DOM 을 생성하여 실제 DOM 에 변경된 결과물을 반영.
  • 렌더와 커밋 단계
    • 렌더 단계 : 상태 변경을 감지해서 변경이 필요한 컴포넌트를 체크 후 이를 기반하여 실제 DOM 에 변경된 결과물을 반영함 (여기서 비교하는 것은 크게 type, props, key)
    • 커밋 단계 : 렌더 단계의 변경 사항을 실제 DOM 에 적용해 사용자에게 보여주는 단계로 변경사항을 계산했는데 아무런 변경사항이 감지되지 않는다면 커밋 단계는 생략될 수 있음. (리액트의 렌더링이 일어난다고 해서 무조건 DOM 업데이트가 일어나는 것은 아니라는 것)


메모이제이션

  • 메모이제이션을 성능 최적화를 위한 기술이지만, 값을 비교하고 렌더링 또는 재계산이 필요한 지 확인하는 작업과 이전에 결과물을 저장해 두었다가 다시 꺼내와야 하는 두 가지 비용이 들기 때문에 두 가지 관점에서 갑론을박이 벌어지고 있음.
    • 주장1 : 섣부른 최적화는 독이다, 꼭 필요한 곳에만 메모이제이션을 추가하자. - 메모이제이션은 값을 어딘가에 저장해두어야 하기 때문에 리소스가 추가로 들고, 성능에 영향이 미미한 작업까지 메모이제이션을 하며 섣부른 최적화는 필요 없다.
    • 주장2 : 렌더링 과정의 비용은 비싸다, 모조리 메모이제이션해 버리자. - 메모이제이션 처리를 하지 않았을 때 치러야 할 잠재적인 위험 비용이 더 크기 때문에 모두 메모이제이션 처리하는 게 더 큰 이점이다.




💭 의견

  • 훅이 등장하면서 함수형 컴포넌트가 득세하게 된 리액트 16.8 이후에 개발을 시작하였기에 클래스 컴포넌트에 대한 지식이 많이 부족하다는 것을 느끼고 있다. 물론 함수형 컴포넌트가 많아졌지만 책에서 말하는 것처럼 현재까지도 클래스로 구현된 컴포넌트들이 많고. (자식 컴포넌트에서 발생한 에러에 대한 처리는 현재 클래스 컴포넌트로만 가능하므로) 에러 처리를 위해서라도 클래스 컴포넌트에 대한 지식은 어느 정도 필요하다고 판단해서 클래스 컴포넌트 공부에 대한 필요성을 느꼈다.
  • 무거운 연산으로 인한 성능 문제를 해결할 수 있는 메모이제이션 처리는 꼭 필요한 기능이지만 불필요한 메모이제이션 처리는 최적화와 개발자 성장에 악영향을 끼친다고 생각한다. 누군가 내 코드를 보고 ‘왜 이렇게 구현했어요?’ 혹은 ‘왜 이걸 썼어요?’ 했을 때 선뜻 대답이 나오지 않는다면 안 쓰는 게 맞다고 생각하는데, ‘무조건 메모이제이션 처리하는 게 성능 상 이점이 있을 것 같아서요’ 는 너무 무책임한 말이지 않은가….. 💭 이슈를 진행하는데 ‘이거 무거워 보이는데?’ 싶어서 테스트 없이 메모이제이션 처리를 하게 되었다면 나중에 리팩토리할 때 꼭 테스트를 진행해보는 게 좋겠다!


❔스터디 1주차 > 질문 정리

  • 🙋🏻‍♀️ JSXElementName > JSXNameSpacedName 은 뭐지?
    • JSXIdentifier:JSXIdentifier의 조합으로 ':' 을 통해 서로 다른 식별자를 이어주는 문법으로 두 개 이상은 불가능
      • JSXIdentifier 는 JSX 내부에서 사용하는 식별자
    • React 에선 사용하지 않는 문법
  • DOM을 방문할 때 display:none과 같이 눈에 보이지 않는 요소는 스킵한다고 했는데
    • 🙋🏻‍♀️ CSSOM은 순회하지 않고 DOM만 방문하는데 해당 정보는 어떻게 하는걸까?
      • 브라우저는 DOM과 CSSOM을 결합하여 렌더 트리를 생성한다. 이 과정에서 display: none과 같은 스타일이 적용된 요소는 렌더 트리에 포함되지 않음. 따라서 브라우저는 DOM을 순회하면서 CSSOM의 정보를 참조하여 렌더 트리를 구성하게 되는 것.
    • 🙋🏻‍♀️ 스타일시트 파싱이 HTML 파싱보다 먼저 일어나는건가?
      • 일반적으로 HTML 파싱과 CSS 파싱은 병렬로 발생. 그러나 CSS 파일이 로드되기 전까지는 렌더링이 차단될 수 있음. 이는 CSS가 렌더 트리에 영향을 미치기 때문.
    • 🙋🏻‍♀️ stylesheet말고 인라인으로 작성하면?
      • 인라인 스타일을 사용해도 display: none이 적용된 요소는 렌더 트리에 포함되지 않으며, 이는 외부 스타일시트와 동일한 동작을 함
    • 🙋🏻‍♀️ 리액트 파이버에서 작업단위를 나누고 우선순위를 매길 때 일시중지하고 재사용 폐기를 하는 그 기준이 뭘까?
      • 아래 참고


📑 리액트 파이버에서 처리하는 우선순위의 기준

  • ✔️ 우선순위를 나누는 기준 값 : expirationTime
    • expirationTime 은 fiber 객체 내부에 있으며, 사용자가 이벤트를 발생시켰을 때의 시점값을 나타냄.
    • expirationTime 이 클수록 먼저 발생한 업데이트라는 것이므로 먼저 처리해야 되는 업데이트로 판단함.
    • expirationTime 이 Sync (동기적으로 처리해야 하는 이벤트) 인 경우 가장 높은 우선순위를 주게 되고, Never 나 Idle (유휴상태) 인 겨우, 낮은 우선순위를 주게 되는데
    • 처리 우선순위가 높은 순서는 “Immediate > UserBlocking > Normal > Idle” 여기서 위를 판단하는 기준이 되는 게 expirationTime
  • ✔️ 기타 참고
    • Legacy mode 에서는 대부분 expirationTime 이 Sync
    • 핵심은 업데이트 유형에 따라 우선 순위가 다르므로 애니메이션 업데이트는 데이터 저장소의 업데이트보다 더 빨리 완료되어야 함.
    • pendingWorkPriority 는 보류 중인 작업 우선순위 파이버가 나타내는 작업의 우선순위를 나타내는 숫자
    • requestAnimationFrame() 는 높은 우선 순위 함수 스케줄링
    • requestIdleCallback() 는 낮은 우선 순위 함수 스케줄링





Reference