TIL: 리액트 딥다이브 (2) 리액트 핵심 요소

Lumpen·2024년 9월 2일
0

TIL

목록 보기
244/244
post-custom-banner

jsx

jsx 는 JSXElement, JSXAtributes, JSXChildren, JSXString 네 가지 컴포넌트로 구성되어 있다

JSX 의 목적은 단순히 HTML, XML 을 자바스크립트 내부에 표현하는 것은 아니다
다양한 속성을 가진 트리 구조를 토큰화해 자바스크립트로 변환하는데 초점을 두고 있다

JSXElement

JSX 를 구성하는 가장 기본 요소로
HTML Element 와 비슷한 역할을 한다

  • JSXOpeningElement: <JSX>
  • JSXClosingElement: </JSX>
  • JSXSelfClosingELement: <JSX />
  • JSXFragment: <></>

react 에서는 대문자로 시작하지 않는 요소명을 html 코드로 인식하기 때문에 컴포넌트 이름을 대문자로 시작해야 한다
JSXElement 표준 내용은 아니고 리액트 고유의 규칙

JSXElementName

요소 이름으로 쓸 수 있는 것들

  • JSXIdentifier: JSX 내부에서 사용할 수 있는 식별자 (자바스크립트 식별자 규칙과 동일
  • JSXNamespacedName: : 로 서로 다른 식별자를 한 번 이어줄 수 있다 <A:B>hi</A:B>
  • JSXMemberExpression: . 로 서로 다른 식별자를 여러번 이어줄 수 있다 <A.B.C>hi</A.B.C>

JSXAtrributes

JSXElement 에 부여할 수 있는 속성을 말한다
속성이기 때문에 필수 요소는 아니다

JSXSpreadAttributes

자바스크립트의 전개 연산자와 동일한 역할
{...AssginmentExpression} 객체뿐 아니라 자바스크립트의 모든 표현식이 존재할 수 있다

JSXAttribute: 속성을 나타내는 키와 값으로 표현한다

<A b='c'></A>

  • JSXAttrributeValue: 문자열, 표현식, JSXelement 중 하나여야 한다
    JSXAttrributeValue 를 사용할 때 JSXelement 를 중괄호로 감싸는 것은 리액트가 아니라 prettier 의 규칙

JSXChildren

JSXElement 의 자식 값을 나타낸다
JSX 는 트리구조를 나타내기 위해 만들어졌기 때문에 부모 자식 관계를 나타낼 수 있다

  • JSXChild: JSXChildren 을 이루는 기본 단위로 JSXChildren 은 JSXChild를 0개 이상 가질 수 있다
  • JSXChildExpression (optional): 자바스크립트 표현식이 자식으로 들어갈 수 있다

JSXStrings

HTML 에서 사용가능한 문자열은 모두 JSXStrings 에서도 사용 가능하다
다만 다른점은 백슬래시를 마음껏 사용 가능하다는 것

JSX가 변환되는 방식

JSX 는 자바스크립트에서 변환된다
@babel/plugin-transfrom-react-jsx 플러그인을 알아야 한다

React JSX

const CompA = `<A required={true}>hi</A>`

변환된 코드

'use strict'
var ComA = React.creatElement(A, {required: true}, 'hi')

리액트 17, 바벨 7.9.0 이후 버전 자동 런타임으로 트랜스파일한 결과는 다음과 같다

'use strict'
var _jsxRuntime = require('custom-jsx-library/jsx-runtime')
var ComA = (0, _jsxRuntime.jsx)(A, {required: true, children: 'hi'})

babel 사용해보기

@babel/plugin-transfrom-react-jsx 를 직접 사용해본다
필요한 패키지를 설치 후

import * as Babel from '@babel/standalone'

Babel.registerPlugin(
	'@babel/plugin-transfrom-react-jsx',
  	require('@babel/plugin-transform-react-jsx')
)

const BABEL_CONFIG = {
	presets: [],
  	plugins: [
    	[
        	'@babel/plugin-transfrom-react-jsx',
          {
            throwIfNamespace: false, 
            runtime: 'automatic', 
            importSource: 'custom-jsx-library'
          }
        ]
    ]
}
const SOURCE_CODE = `const CompA = <A>hi</A>`

const {code} = Babel.transform(SOURCE_CODE, BABEL_CONFIG)
// code 에 트랜스파일된 결과가 담긴다

결과물에 약간 차이가 있지만 공통점이 있다

  1. JSXElement 를 첫 번쨰 인수로 선언해 요소를 정의한다
  2. 옵셔널인 JSXChildren, JSXAttributes, JSXStrings 는 이후 인수로 넘겨주어 처리한다

경우에따라 JSXElement 만 다르게 써야할 경우 유용하게 사용할 수 있다
JSX 값은 React.createElement 로 반환되기 때문에

return isHeding ? (<h1 className='text'>hi</h1>):(<span className='text'>hi</span>)

return createElement(isHeading ? 'h1' : 'span', {className: 'text'}, hi)

둘은 같은 결과를 준다

가상 DOM 과 리액트 파이버

리액트의 특징은 가상 DOM 을 사용한다는 점이다

DOM 과 브라우저 렌더링

Document Object Model 이란 웹페이지에 대한 인터페이스로
브라우저가 웹페이지의 콘텐츠와 구조를 어떻게 보여줄지에 대한 정보를 담고 있다

브라우저 렌더링 과정

  1. 요청한 주소에 접근해 HTML 파일 다운로드
  2. 렌더링 엔진이 HTML 을 파싱해 DOM 노드로 구성된 DOM 트리를 만든다
  3. 2번 과정 중 CSS 를 만나면 CSS 파일 다운로드
  4. CSS 파싱하여 CSS 노드로 구성된 CSS 트리(CSSOM) 를 만든다
  5. 2번에서 만든 DOM 노드 중 사용자에게 보여줄 노드만 순회한다
  6. 5번에서 제외된 보여지는 노드를 대상으로 CSSOM 정보를 찾아 스타일 정보를 노드에 적용한다 (layout, painting)
  • layout 과정: 각 노드가 위치할 좌표를 계산한다
  • painting 과정: 레이아웃 단계를 거친 노드의 모습을 그린다

위와 같은 과정은 인터랙티브한 웹의 경우
레이아웃과 리페인팅에 발생하는 비용이 크다
자식 요소가 많은 경우 더욱 그렇다
싱글페이지 어플리케이션의 경우에는 더더욱 비용이 커진다

이러한 문제를 해결하고자 탄생한 것이 가상 DOM 이다
가상 DOM 은 브라우저에서 관리하는 것이 아니라
React 에서 관리한다
실제 DOM 과 같은 객체를 리액트에서 가지고 있다가 변경점을 변경 후 실제 DOM 에 적용시킨다

가상 DOM 의 오해는 빠르다는 것이다
항상 빠른 것은 아니고 대부분의 상황에서 충분히 빠르다

리액트 파이버

가상 DOM 을 위한 아키텍처로 가상 DOM 과 렌더링을 최적화 한다

리액트 파이버는 리액트에서 관리하는 자바스크립트 객체
파이버는 파이버 재조정자가 관리하는데 가상 DOM 과 실제 DOM 을 비교해 변경 사항을 수집, 변경점에 대한 렌더링을 요청한다
리액트에서 발생하는 애니매이션, 레이아웃, 인터랙션에 결과물을 만드는 반응성 문제를 해결한다

파이버는 다음과 같은 일을 한다

  • 작업을 작은 단위로 분할, 우선순위를 매긴다
  • 위 작업을 일시 중지하고 다시 시작할 수 있다
  • 이전해 했던 작업을 다시 재사용하거나 폐기할 수 있다

파이버의 모든 과정은 비동기로 일어난다
과거에는 스택으로 이루어져 있었는데 비효율적이라 교체됨

파이버는 하나의 작업 단위로 구성돼 있다
작업 단위를 하나씩 처리하고 finisheWork() 라는 작업으로 마무리한다
이 작업을 커밋하여 브라우저 DOM 에게 알린다

  • 렌더 단계에서 리액트는 사용자에게 보이지 않는 모든 비동기 작업을 수행한다. 그리고 파이버의 작업 수행(우선순위, 일시 정지, 폐기 등)
  • 커밋 단계에서는 DOM 에 실제 변경사항을 반영하기 위한 commitWork() 가 실행된다. 이는 동기식으로 일어나고 중단되지 않는다

파이버는 컴포넌트가 마운트되는 시점에 생성되어 최대한 재활용 된다
파이버는 컴포넌트와 1:1 관계
state 가 변경되거나 생명주기 메서드, DOM 변경이 필요한 시점 등에 실행된다

리액트 파이버 트리

리액트의 파이버 트리는 내부에 두 개가 존재한다
하나는 현재 모습에 대한 트리
하나는 작업중인 상태에 대한 트리 (workInProgress 트리)
리액트 파이버의 작업이 끝나면 리액트의 포인터만 변경해
workInProgress 의 트리를 현재 트리로 바꿔버린다
이러한 기술을 더블 버퍼링이라고 한다
더블 버퍼링은 원래는 컴퓨터 그래픽 분야에서 사용하는 용어
미처 다 그리지 못한 모습을 노출시키지 않기 위해 더블 버퍼링을 사용한다
커밋 단계 이후에 수행된다
먼저 현재 UI 렌더링을 위해 존재하는 current 트리를 기준으로 모든 작업이 시작된다
업데이트 발생 시 파이버는 리액트에서 새로 받은 데이터로 workInProgress 트리를 작업 후 빌드가 끝나면
다음 렌더링에 변경된 트리를 사용한다

지금까지 리액트가 변경점만 찾아서 업데이트 한다고 생각했는데
반은 맞고 반은 틀렸다고 볼 수 있는..?
기존 트리를 복사한 트리에서 변경점을 변경 후 참조를 변경하는 것이었다

파이버의 작업 순서

  1. 리액트에서 beginWork() 함수를 실행하여 파이버의 작업을 수행하는데, 더 이상 자식이 없는 파이버를 만날 때까지 트리 형식으로 시작
  2. 1번에서 작업이 끝나면 completeWork() 함수를 실행해 파이버 작업을 완료한다
  3. 형제가 있다면 형제로 넘어간다
  4. 2, 3번이 모두 끝난 뒤 return 으로 돌아가 자신의 작업이 완료되었음을 알린다

current 파이버 트리가 위 작업으로 생성된다
setState 로 상태가 업데이트 되면 worInProgress 트리를 빌드
빌드 과정은 같다
최초 렌더링 시 current는 모든 파이버를 새롭게 만들어야 하지만
worInProgress는 기존 파이버에서 업데이트된 props 를 받아 파이버 내부에서 처리한다

기존에는 위 과정을 동기식으로 했지만
현재는 비동기로 우선순위에 따라 비동기식으로 수행된다
우선순위에 따라 현재 업데이트를 일시 중지, 새롭게 변경하거나, 폐기할 수도 있다

파이버와 가상 DOM

리액트 컴포넌트에 대한 정보를 1:1 로 가지고 있는 것이 파이버고
파이버는 리액트 아키텍처 내부에서 비동기로 이뤄진다
실제 브라우저 구조인 DOM 에 반영하는 것은 동기적으로 일어나야 하고
처리하는 작업이 많아 불완전한 작업들은 가상에서(메모리에서) 수행해 최종 결과물만 실제 브라우저 DOM 에 적용하는 것이다

가상 DOM 이라는 것은 웹 애플리케이션에서만 통용되는 개념이다
리액트 파이버는 브라우저 환경에서도 사용할 수 있기 때문에
가상 DOM 과 파이버는 동일 개념은 아니다

클래스 컴포넌트의 한계

클래스 컴포넌트에서 제공하는 메서드만으로도 완성도 있는 애플리케이션을 만들 수 있다
그런데도 함수 컴포넌트를 사용하는 이유는 아마도 클래스 컴포넌트가 갖는 한계 때문일 것이다

  • 데이터 흐름 추적이 어렵다
  • 애플리케이션 내부 로직의 재사용이 어렵다
  • 컴포넌트의 크기가 커진다
  • 클래스가 함수에 비해 상대적으로 어렵다
  • 코드 크기를 최적화하기 어렵다 (번들 크기를 줄이기 어렵다) - 트리 쉐이킹이 되지 않는 코드들이 있음
  • 핫 리로딩에 불리하다 - 코드 변경 시 앱을 다시 시작하지 않고 변경된 코드만 업데이트하는 핫 리로딩이 어렵다.

함수 컴포넌트

  • 함수 컴포넌트에는 생명주기 메서드가 존재하지 않는다
    useEffect 로 비슷하게 구현할 수 있지만 같지 않고
    useEffect 는 생명주기를 위한 훅이 아니다

  • 함수 컴포넌트는 렌더링된 값을 고정한다
    클래스 컴포넌트는 this 를 사용하기 때문에 컴포넌트 인스턴스의 멤버는 변경 가능한 값이다

리액트 렌더링

브라우저 렌더링이란 HTML, CSS 리소스를 기반으로 웹페이지에
필요한 UI 를 그리는 과정을 의미한다
리액트의 렌더링이란 브라우저 렌더링에 필요한 DOM 트리를 그리는 것이다
리액트 렌더링 프로세스를 이해하는 것은 곧 리액트를 이해하는 첫 걸음이다
리액트의 렌더링은 사용자에게 시간과 리소스를 소비하도록 하기 때문에
사용자 경험에 중요한 요소이다

리액트에서의 렌더링이란 리액트 애플리케이션 안에 있는 컴포넌트들이
자신들이 가지고 있는 props 와 state 값을 기반으로 어떻게 UI 를 구성하고 어떤 DOM 결과를 브라우저에게 제공할 것인지 계산하는 일련의 과정이다

props 와 state 를 가지고 있지 않다면 해당 컴포넌트가 반환하는 JSX 값에 기반해 렌더링이 일어나게 된다

리액트 렌더링의 발생

  1. 최초 렌더링: 사용자에게 처음 진입 시 보여줄 렌더링으로 구성된 JSX 값에 의한 렌더링
  2. 리렌더링: 첫 진입 이후의 모든 렌더링으로 state 가 업데이트 될 때 일어난다

리렌더링 발생 조건

  • useState: useState() 의 setter 가 실행되어 상태가 업데이트 되는 경우

  • useReducer: useReducer() 의 dispatch 가 실행되어 상태가 업데이트 되는 경우

  • key props: 컴포넌트의 key props 가 변경되는 경우 (key 는 명시적으로 선언되어있지 않더라도 모든 컴포넌트에서 사용한다. 배열에서 하위 컴포넌트 선언 시 사용 - 리액트는 배열 요소를 key 로 관리한다)

  • props: 부모로부터 전달받는 props 가 변경되는 경우

  • 부모 컴포넌트의 리렌더링: 부모 컴포넌트가 리렌더링될 경우 모든 자식 컴포넌트에서 리렌더링이 일어난다

리액트의 렌더링 프로세스

렌더링 프로세스가 시작되면 리액트는 컴포넌트 루트부터 업데이트가 필요한 모든 컴포넌트를 찾는다
업데이트가 필요한 컴포넌트를 발견하면 FunctionComponent() 를 호출한 뒤에 결과물을 저장한다
렌더링 결과물은 JSX 문법으로 구성되어 있고 JSX 문법이 자바스크립트로 컴파일되면서 React.createElement() 를 호출하는 구문으로 변환된다
createElement 는 자바스크립트 객체를 반환한다

렌더링 프로세스가 실행되면서 이런 과정을 거쳐 각 컴포넌트 렌더링 결과물을 수집한 후 리액트의 새로운 트리인 가상 DOM 과 실제 DOM 을 비교해 모든 변경 사항을 수집한다
이렇게 계산하는 과정을 리액트의 재조정이라고 한다
재조정이 끝나면 모든 변경 사항을 하나의 동기 시퀀스로 DOM 에 적용되어 변경된 결과가 반여오딘다

리액트의 렌더링은 렌더 단계와 커밋 단계로 분리되어 실행된다

렌더와 커밋

렌더 단계

컴포넌트를 렌더링하고 변경 사항을 계산하는 모든 작업을 말한다
컴포넌트를 실행(render() 또는 return) 후의 결과와 이전 가상 DOM 을 비교하는 과정을 거쳐 변경이 필요한 컴포넌트를 체크하는 단계다
비교하는 것은 크게 type, props, key 세 가지다
하나라도 변경된 것이 있으면 변경이 필요한 컴포넌트로 체크한다

커밋 단계

렌더 단계의 변경 사항을 실제 DOM 에 적용해 사용자에게 보여주는 과정이다
이 단계가 끝나야 브라우저 렌더링이 발생한다

리액트가 먼저 DOM 을 커밋 단계에서 업데이트 하면
이렇게 만들어진 모든 DOM 노드 및 인스턴스를 가리키도록
리액트 내부 참조를 업데이트 한다
그 다음 useLayoutEffect 훅을 호출한다

리액트의 렌더링이 일어난다고 무조건 DOM 업데이트가 되는 것은 아니다
변경 사항이 감지되지 않는다면 커밋 단계는 생략될 수 있다
리액트의 렌더링은 꼭 가시적인 변경이 일어나지 않아도 발생할 수 있다

기존의 렌더링은 항상 동기식으로 작동했다
이는 성능 저하를 가져올 수 있다
그러나 몇 가지 상황에서는 동기식으로 작동하는 편이 유리할 수도 있다
비동기 렌더링에 우선순위를 반영하는 동시성 렌더링이 리액트 18에서 도입되었다

리액트의 컴포넌트 렌더링 순서는 부모에서 자식으로 이어진다
부모 컴포넌트 렌더링 후 자식 컴포넌트 렌더링

만약 부모 컴포넌트와 자식 컴포넌트 모두 리렌더링 해야 할 경우
부모가 렌더링을 일으켜 자식도 렌더링 된 후에 자식 컴포넌트도 자신의 업데이트 적용하기 위해 리렌더링 된다

메모이제이션

너주 자주 리렌더링이 일어나는 컴포넌트는 useMemo, useCallback 등을 이용해 메모이제션을 하는 것이 좋을 수 있다

메모이제이션이란 메모리 상에 자주 쓰는 것들을 저장해놓고 다음에 다시 사용하는 것이다
때문에 매모리 관리와 클라이언트 상에서 렌더링하는 비용의 트레이드 오프 관계를 잘 이해하고 사용해야 한다
책에서는 판단이 어렵다면 메모이제이션 하는 편이 더 효율적일 확률이 높다는 의견을 주었다

profile
떠돌이 생활을 하는. 실업자, 부랑 생활을 하는
post-custom-banner

0개의 댓글