jsx 는 JSXElement, JSXAtributes, JSXChildren, JSXString 네 가지 컴포넌트로 구성되어 있다
JSX 의 목적은 단순히 HTML, XML 을 자바스크립트 내부에 표현하는 것은 아니다
다양한 속성을 가진 트리 구조를 토큰화해 자바스크립트로 변환하는데 초점을 두고 있다
자바스크립트 내에서 표현하기 까다로운 XML 스타일의 트리 구문을 작성하기 편리하도록 만든문법
JSX 를 구성하는 가장 기본 요소로
HTML Element 와 비슷한 역할을 한다
<JSX>
</JSX>
<JSX />
<></>
react 에서는 대문자로 시작하지 않는 요소명을 html 코드로 인식하기 때문에 컴포넌트 이름을 대문자로 시작해야 한다
JSXElement 표준 내용은 아니고 리액트 고유의 규칙
요소 이름으로 쓸 수 있는 것들
:
로 서로 다른 식별자를 한 번 이어줄 수 있다 <A:B>hi</A:B>
.
로 서로 다른 식별자를 여러번 이어줄 수 있다 <A.B.C>hi</A.B.C>
JSXElement 에 부여할 수 있는 속성을 말한다
속성이기 때문에 필수 요소는 아니다
자바스크립트의 전개 연산자와 동일한 역할
{...AssginmentExpression}
객체뿐 아니라 자바스크립트의 모든 표현식이 존재할 수 있다
<A b='c'></A>
JSXElement 의 자식 값을 나타낸다
JSX 는 트리구조를 나타내기 위해 만들어졌기 때문에 부모 자식 관계를 나타낼 수 있다
HTML 에서 사용가능한 문자열은 모두 JSXStrings 에서도 사용 가능하다
다만 다른점은 백슬래시를 마음껏 사용 가능하다는 것
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/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 에 트랜스파일된 결과가 담긴다
결과물에 약간 차이가 있지만 공통점이 있다
경우에따라 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 을 사용한다는 점이다
Document Object Model 이란 웹페이지에 대한 인터페이스로
브라우저가 웹페이지의 콘텐츠와 구조를 어떻게 보여줄지에 대한 정보를 담고 있다
위와 같은 과정은 인터랙티브한 웹의 경우
레이아웃과 리페인팅에 발생하는 비용이 크다
자식 요소가 많은 경우 더욱 그렇다
싱글페이지 어플리케이션의 경우에는 더더욱 비용이 커진다
이러한 문제를 해결하고자 탄생한 것이 가상 DOM 이다
가상 DOM 은 브라우저에서 관리하는 것이 아니라
React 에서 관리한다
실제 DOM 과 같은 객체를 리액트에서 가지고 있다가 변경점을 변경 후 실제 DOM 에 적용시킨다
가상 DOM 의 오해는 빠르다는 것이다
항상 빠른 것은 아니고 대부분의 상황에서 충분히 빠르다
가상 DOM 을 위한 아키텍처로 가상 DOM 과 렌더링을 최적화 한다
리액트 파이버는 리액트에서 관리하는 자바스크립트 객체
파이버는 파이버 재조정자 가 관리하는데 가상 DOM 과 실제 DOM 을 비교해 변경 사항을 수집, 변경점에 대한 렌더링을 요청한다
리액트에서 발생하는 애니매이션, 레이아웃, 인터랙션에 결과물을 만드는 반응성 문제를 해결한다
리액트 파이버 재조정자는 가상 DOM 과 실제 DOM 을 비교하는 알고리즘이라고 볼 수 있다
파이버는 다음과 같은 일을 한다
파이버의 모든 과정은 비동기로 일어난다
과거에는 스택으로 이루어져 있었는데 비효율적이라 교체됨
파이버는 하나의 작업 단위로 구성돼 있다
작업 단위를 하나씩 처리하고 finisheWork() 라는 작업으로 마무리한다
이 작업을 커밋하여 브라우저 DOM 에게 알린다
파이버는 리액트 요소와 유사하다고 느낄 수 있지만
리액트 요소는 렌더링 발생 시마다 새로 생성되지만
파이버는 컴포넌트가 마운트되는 시점에 생성되어 최대한 재활용 된다
파이버는 컴포넌트와 1:1 관계, tag 로 연결되어 있다
FunctionComponent
ClassComponent
HostComponet (HTML 요소)
등
stateNode: 이 속성에는 파이버 자체에 대한 참조 정보를 가진다 이 참조를 바탕으로 리액트가 파이버 상태에 접근한다
child, silbling, return: 파이버 간 관계를 나타내는 속성으로 파이버도 리액트 컴포넌트 트리와 동일한 파이버 트리 형식을 갖게된다 다만 children 이 아닌 child 로 하나의 자식 정보만 존재한다
sbliing 은 형제, return 은 부모 파이버를 말한다
sibling 은 이전 요소가 다음 요소에 대한 참조만을 가진 체인 형식으로 마지막 형제 파이버는 silbling 참조를 가지고 있지 않다
각 형제들은 index 로 순서를 가지고 있다
부모 파이버에서는 형제 중 첫번째 파이버만 참조한다
pendingProps 는 아직 처리하지 못한 props로
모두 처리되면 memorizedProps 로 이동
state 가 변경되거나 생명주기 메서드, DOM 변경이 필요한 시점 등에 실행된다
리액트의 파이버 트리는 내부에 두 개가 존재한다
하나는 현재 모습에 대한 트리
하나는 작업중인 상태에 대한 트리 (workInProgress 트리)
리액트 파이버의 작업이 끝나면 리액트의 포인터만 변경해
workInProgress 의 트리를 현재 트리로 바꿔버린다
이러한 기술을 더블 버퍼링
이라고 한다
더블 버퍼링은 원래는 컴퓨터 그래픽 분야에서 사용하는 용어
미처 다 그리지 못한 모습을 노출시키지 않기 위해 더블 버퍼링을 사용한다
커밋 단계 이후에 수행된다
먼저 현재 UI 렌더링을 위해 존재하는 current 트리를 기준으로 모든 작업이 시작된다
업데이트 발생 시 파이버는 리액트에서 새로 받은 데이터로 workInProgress 트리를 작업 후 빌드가 끝나면
다음 렌더링에 변경된 트리를 사용한다
지금까지 리액트가 변경점만 찾아서 업데이트 한다고 생각했는데
반은 맞고 반은 틀렸다고 볼 수 있는..?
기존 트리를 복사한 트리에서 변경점을 변경 후 참조를 변경하는 것이었다
current 파이버 트리가 위 작업으로 생성된다
setState 로 상태가 업데이트 되면 worInProgress 트리를 빌드
빌드 과정은 같다
최초 렌더링 시 current는 모든 파이버를 새롭게 만들어야 하지만
worInProgress는 기존 파이버에서 업데이트된 props 를 받아 파이버 내부에서 처리한다
기존에는 위 과정을 동기식으로 했지만
현재는 비동기로 우선순위에 따라 비동기식으로 수행된다
우선순위에 따라 현재 업데이트를 일시 중지, 새롭게 변경하거나, 폐기할 수도 있다
리액트 컴포넌트에 대한 정보를 1:1 로 가지고 있는 것이 파이버고
파이버는 리액트 아키텍처 내부에서 비동기로 이뤄진다
실제 브라우저 구조인 DOM 에 반영하는 것은 동기적으로 일어나야 하고
처리하는 작업이 많아 불완전한 작업들은 가상에서(메모리에서) 수행해 최종 결과물만 실제 브라우저 DOM 에 적용하는 것이다
가상 DOM 이라는 것은 웹 애플리케이션에서만 통용되는 개념이다
리액트 파이버는 브라우저 환경에서도 사용할 수 있기 때문에
가상 DOM 과 파이버는 동일 개념은 아니다
리액트의 에러 바운더리는 아직 hooks 로 구현되지 않은 클래스 컴포넌트의 생명주기를 사용한다
정상적인 생명 주기에서 벗어난 에러 상황에서 실행되는 메서드
자식 컴포넌트에서 자바스크립트 에러가 발생하였을 때 에러를 캐치하여 에러 처리 로직을 구현할 수 있다
static 메서드로 error 를 인수로 받는다
실행 시점이 에러 발생 시 자식 컴포넌트에 대한 렌더링을 결정하기 때문에 반드시 state 를 반환해야 한다
자식 컴포넌트에서 에러가 발생했을 때 실행되며
getDerivedStateFromError 에서 에러를 잡고
반환되는 state 가 결정된 이후에 실행된다
error 와 에러의 정보를 담은 info 를 인수로 받는다
render 단계에서 실행되는 getDerivedStateFromError 와는 다르게
커밋 단계에서 실행되기 때문에 에러에 대한 부수효과를 실행할 수 있다
Function.name 혹은 DisplayName 으로 에러의 위치를 표현해준다
때문에 memo(() => {}) 등과 같이 컴포넌트 명을 추론할 수 없는 경우는 에러 발생 영역을 정확히 알기 어렵다
위 두 메서드는 ErrorBoundary 를 만드는 용도로 많이 사용된다
ErrorBoundary 는 내부 영역에 대한 에러만 잡을 수 있다
떄문에 여러 영역에 대해 세분화하여 사용 가능하다
import React, { PropsWithChildren } from 'react'
type Props = PropsWithChildren({})
type State = { hasError: boolean, errorMessage: string }
export default class ErrorBoundary extends React.Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = {
hasError: false,
errorMessage: '',
}
}
static getDerivedStateFromError(error: Error) {
return {
hasError: true,
errorMessage: '',
}
}
componentDidError(error: Error, info: ErrorInfo) {
console.log(error)
console.log(info)
}
render() {
if (this.state.hasError) {
return (
<div>
<h1>에러 발생</h1>
<p>{this.state.errorMessage}<p/>
</div>
)
}
}
return this.props.children
// 일반적인 상황에서의 return
}
클래스 컴포넌트에서 제공하는 메서드만으로도 완성도 있는 애플리케이션을 만들 수 있다
그런데도 함수 컴포넌트를 사용하는 이유는 아마도 클래스 컴포넌트가 갖는 한계 때문일 것이다
함수 컴포넌트에는 생명주기 메서드가 존재하지 않는다
useEffect 로 비슷하게 구현할 수 있지만 같지 않고
useEffect 는 생명주기를 위한 훅이 아닌 state 변경에 따라 부수 효과를 일으키기 위한 hook
함수 컴포넌트는 렌더링된 값을 고정한다
클래스 컴포넌트는 this 를 사용하기 때문에 컴포넌트 인스턴스의 멤버는 변경 가능한 값이다
브라우저 렌더링이란 HTML, CSS 리소스를 기반으로 웹페이지에
필요한 UI 를 그리는 과정을 의미한다
리액트의 렌더링이란 브라우저 렌더링에 필요한 DOM 트리를 그리는 것이다
리액트 렌더링 프로세스를 이해하는 것은 곧 리액트를 이해하는 첫 걸음이다
리액트의 렌더링은 사용자에게 시간과 리소스를 소비하도록 하기 때문에
사용자 경험에 중요한 요소이다
리액트에서의 렌더링이란 리액트 애플리케이션 안에 있는 컴포넌트들이
자신들이 가지고 있는 props 와 state 값을 기반으로 어떻게 UI 를 구성하고 어떤 DOM 결과를 브라우저에게 제공할 것인지 계산하는 일련의 과정이다
props 와 state 를 가지고 있지 않다면 해당 컴포넌트가 반환하는 JSX 값에 기반해 렌더링이 일어나게 된다
useState: useState() 의 setter 가 실행되어 상태가 업데이트 되는 경우
useReducer: useReducer() 의 dispatch 가 실행되어 상태가 업데이트 되는 경우
key props: 컴포넌트의 key props 가 변경되는 경우 (key 는 명시적으로 선언되어있지 않더라도 모든 컴포넌트에서 사용한다. 배열에서 하위 컴포넌트 선언 시 사용 - 리액트는 배열 요소를 key 로 관리한다)
props: 부모로부터 전달받는 props 가 변경되는 경우
부모 컴포넌트의 리렌더링: 부모 컴포넌트가 리렌더링될 경우 모든 자식 컴포넌트에서 리렌더링이 일어난다
렌더링 프로세스가 시작되면 리액트는 컴포넌트 루트부터 업데이트가 필요한 모든 컴포넌트를 찾는다
업데이트가 필요한 컴포넌트를 발견하면 FunctionComponent() 를 호출한 뒤에 결과물을 저장한다
렌더링 결과물은 JSX 문법으로 구성되어 있고 JSX 문법이 자바스크립트로 컴파일되면서 React.createElement() 를 호출하는 구문으로 변환된다
createElement 는 자바스크립트 객체를 반환한다
function Hello() {
return (
<TestComponent a={35} b={'b'}>
hi
</TestComponent>
)
}
위 JSX 문법은 React.createElement 를 호출하여
자바스크립트 객체로 변환된다
function Hello() {
return React.createElement(
TestComponent,
{ a:35, b: 'b' },
'hi'
)
}
{type: TestComponent, props: { a: 35, b: 'b' }, children: 'hi' }
렌더링 프로세스가 실행되면서 이런 과정을 거쳐 각 컴포넌트 렌더링 결과물을 수집한 후 리액트의 새로운 트리인 가상 DOM 과 실제 DOM 을 비교해 모든 변경 사항을 수집한다
이렇게 계산하는 과정을 리액트의 재조정이라고 한다
재조정이 끝나면 모든 변경 사항을 하나의 동기 시퀀스로 DOM 에 적용되어 변경된 결과가 반여오딘다
리액트의 렌더링은 렌더 단계와 커밋 단계로 분리되어 실행된다
컴포넌트를 렌더링하고 변경 사항을 계산하는 모든 작업을 말한다
컴포넌트를 실행(render() 또는 return) 후의 결과와 이전 가상 DOM 을 비교하는 과정을 거쳐 변경이 필요한 컴포넌트를 체크하는 단계다
비교하는 것은 크게 type, props, key 세 가지다
하나라도 변경된 것이 있으면 변경이 필요한 컴포넌트로 체크한다
렌더 단계의 변경 사항을 실제 DOM 에 적용해 사용자에게 보여주는 과정이다
이 단계가 끝나야 브라우저 렌더링이 발생한다
리액트가 먼저 DOM 을 커밋 단계에서 업데이트 하면
이렇게 만들어진 모든 DOM 노드 및 인스턴스를 가리키도록
리액트 내부 참조를 업데이트 한다
그 다음 useLayoutEffect 훅을 호출한다
리액트의 렌더링이 일어난다고 무조건 DOM 업데이트가 되는 것은 아니다
변경 사항이 감지되지 않는다면 커밋 단계는 생략될 수 있다
리액트의 렌더링은 꼭 가시적인 변경이 일어나지 않아도 발생할 수 있다
기존의 렌더링은 항상 동기식으로 작동했다
이는 성능 저하를 가져올 수 있다
그러나 몇 가지 상황에서는 동기식으로 작동하는 편이 유리할 수도 있다
비동기 렌더링에 우선순위를 반영하는 동시성 렌더링이 리액트 18에서 도입되었다
필요하다면 렌더링의 중단 및 재시작, 포기할 수도 있다
리액트의 컴포넌트 렌더링 순서는 부모에서 자식으로 이어진다
부모 컴포넌트 렌더링 후 자식 컴포넌트 렌더링
만약 부모 컴포넌트와 자식 컴포넌트 모두 리렌더링 해야 할 경우
부모가 렌더링을 일으켜 자식도 렌더링 된 후에 자식 컴포넌트도 자신의 업데이트 적용하기 위해 리렌더링 된다
너주 자주 리렌더링이 일어나는 컴포넌트는 useMemo, useCallback 등을 이용해 메모이제션을 하는 것이 좋을 수 있다
메모이제이션이란 메모리 상에 자주 쓰는 것들을 저장해놓고 다음에 다시 사용하는 것이다
때문에 매모리 관리와 클라이언트 상에서 렌더링하는 비용의 트레이드 오프 관계를 잘 이해하고 사용해야 한다
책에서는 판단이 어렵다면 메모이제이션 하는 편이 더 효율적일 확률이 높다는 의견을 주었다