본문 바로가기
React-study/dil

[모던 리액트 Deep Dive] 2장 리액트 핵심 요소 깊게 살펴보기

by 어느새벽 2024. 11. 6.

2.1 JSX란

2.1.1 JSX의 정의

  • JSX는 자바스크립트 표준이 아닌 페이스북 팀에서 만든 새로운 규칙이며, 반드시 자바스크립트 코드로 변환되어야 한다.
  • JSX의 목표는 JSX 내부에 트리 구조로 표현하고 싶은 다양한 것들을 작성해 두고, 이 JSX를 트랜스파일이라는 과정을 거쳐 자바스크립트가 이해할 수 있는 코드로 변경하는 것이다. 
  • JSX는 JSXElement, JSXAttributes, JSXChildren, JSXString 네 가지로 구성되어 있으며, 개발자는 이는 확장성이 용이하게 디자인된 문법으로 이해할 필요가 있다 .
JSXOpeningElement <div>
JSXClosingElement </div>
JSXSelfClosingElement <div />
JSXFragment <>children</>

 

  • 사용자가 컴포넌트를 만들어 사용할 때에는 반드시 대문자로 시작하는 컴포넌트여야 한다. 이는 리액트에서 HTML 태그명과 사용자가 만든 컴포넌트 태그명을 구분 짓기 위해서다.

JSXElement

JSXIdentifier : JSX내부에서 사용할 수 있는 식별자를 의미한다. <$></$> <_></_>도 가능하지만 자바스크립트와 마찬가지로 숫자로 시작하거나 $와 _외의 다른 특수문자로는 시작할 수 없다. ❓

function Valid() {
	return <$></$>
	return <_></_>
}

//불가능
function Invalid1() {
	return <1></1>
}

 

JSXNamespacedName : JSXIdentifier : JSXIdentifier의 조합, 즉 :을 통해 서로 다른 식별자를 이어주는 것도 하나의 식별자로 취급된다. 두개 이상은 취급하지 않는다.

function valid() {
	return <foo:bar></foo:bar>
}

//불가능
function invalid() {
	return <foo:bar:baz></foo:bar:baz>
}

 

JSXMemberExpression: JSXIdentifier.JSXIdentifier의 조합, 즉 .을 통해 서로 다른 식별자를 이어주는 것도 하나의 식별자로 취급한다. ❓

function valid1() {
	return <foo.bar></foo.bar>
}

//여러개 이어서 가능
function valid2() {
	return <foo.bar.baz></foo.bar.baz>
}

//불가능
function invalid() {
	return <foo:bar.baz></foo:bar.baz>
}

 

JSXAttributes

  • JSXElement에 부여할 수 있는 속성을 의미한다. 단순히 속성을 의미하기 때문에 존재하지 않아도 에러가 나지 않는다.

JSXSpreadAttributes

  • 자바스크립트의 전개 연산자와 동일한 역할을 한다.
  • {...AssignmentExpression}: 이 AssignmentExpression에는 단순히 객체뿐만 아니라 자바스크립트에서 AssignmentExpression으로 취급되는 모든 표현식이 존재할 수 있다. 조건문 표현식, 화살표 함수, 할당식 등 다양한 것이 포함된다.

JSXAtributes

  • 속성을 나타내는 키와 값으로 짝을 이루어서 표현한다. 키는 JSXAttributeName, 값은 JSXAttributeValue로 불린다.
  • JSXAttributeName: 속성의 키 값. 키로는 앞서 JSXElementName에서 언급했던 JSXIdentifier와 JSXNamespacedName이 가능하다. : 을 이용해 키를 나타낼 수 있다.
function valid() {
	return <foo.bar foo:bar="baz"></foo.bar>
}
  • JSXAttributeValue: 속성의 키에 할당할 수 있는 값으로 "큰따옴표로 구성된 문자열" or '작은따옴표로 구성된 문자열' 둘 중 하나를 만족해야 한다.
  • { AssignmentExpression }: 자바스크립트의 AssignmentExpression을 의미한다. 자바스크립트에서 변수에 값을 넣을 수 있는 표현식은 JSX 속성의 값으로도 가능하다.
  • JSXElement: 값으로 다른 JSX 요소가 들어갈 수 있다.
function Child({ attribute }) {
	return <div>{attribute}</div>
}

export default function App() {
	return (
		<div>
			<Child attribute=<div>hello</div> />
		</div>
	)
}

 

JSXChildtren

  • JSXElement의 자식 값을 나타낸다. 

JSXChild

  • JSXChildren을 이루는 기본 단위다. JSXChildren은 JSChild가 없어도 상관없다.
  • JSXText: {, <, >, }을 제외한 문자열. 앞에 기호를 표현하려면 문자열로 표시하면 된다.
  • JSXElement: 값으로 다른 JSX 요소가 들어갈 수 있다.
  • JSXFragment: 값으로 빈 JSX요소인 <></>가 들어갈 수 있다.
  • { JSXChildExpression (optional) } : 자바스크립트의 AssignmentExpression을 의미한다.
function App() {
 return <>{(() => 'foo')()}</>
}
//리액트에서 렌더링하면 "foo"라는 문자열이 출력된다.

 

JSXStrings

  • HTML에서 사용 가능한 문자열은 모두 JSXStrings에서도 가능하다.
  • 큰따옴표 혹은 작은따옴표로 구성된 문자열 혹은 JSXText를 의미한다.
  • 자바스크립트와 한 가지 중요한 차이점이 발생하는데, JSX에서는 \를 사용할 수 있다.
 <button>/</button> // 가능
 
 let escape1 = "\" // SyntaxError
 let escape2 = "\\" // 가능

 

JSX는 어떻게 JS에서 변환될까?

  • @babel/plugin-transform-jsx는 JSX구문을 자바스크립트가 이해할 수 있는 형태로 변환한다.
//JSX 코드

const ComponentA = <A required={true}>Hello World</A>;
const ComponentB = <>Hello World</>;
const ComponentC = (
     <div>
         <span>hello world</span>
     </div>
 )

 

@babel/plugin-transform-jsx로 변환하면 아래와 같다.

'use strict'
 
var ComponentA = React.createElement(
     A,
     {
         required: true,
     },
     'Hello World',
 )
var ComponentB = React.createElement(React.Fragment, null, 'Hello World')
var ComponentC = React.createElement(
     'div',
     null,
     React.createElement('span', null, 'hello world'),
 )
  • JSX 반환값이 결국 React.createElement로 귀결된다는 사실을 파악한다면 쉽게 리팩터링할 수 있다.

2.2 가상 DOM과 리액트 파이버

  • 리액트의 대표적인 특징은 실제 DOM이 아닌 가상 DOM을 운영한다는 것이다.

DOM과 브라우저 렌더링 과정

DOM(Document Object Model)은 웹페이지에 대한 인터페이스로 브라우저가 웹페이지의 콘텐츠와 구조를 어떻게 보여줄지에 대한 정보를 담고 있다. 다음은 브라우저가 웹사이트 접근 요청을 받고 화면을 그리는 과정이다.

 

 

  • HTML 파일 다운로드: 사용자가 웹 주소에 들어가면 브라우저는 먼저 HTML 파일을 다운로드한다. HTML은 웹사이트의 뼈대 역할을 한다.
  • DOM 트리 만들기: 브라우저는 HTML을 분석해서 DOM이라는 구조를 만든다. DOM은 HTML의 각 요소(예: 제목, 본문, 이미지 등)를 트리 구조로 표현한 것이다.
  • CSS 파일 다운로드: HTML을 분석하다가 CSS 파일을 만나면, CSS도 다운로드한다. CSS는 각 요소에 스타일(색, 글자 크기, 위치 등)을 적용하는 역할을 한다.
  • CSSOM 트리 만들기: 브라우저는 CSS를 분석해서 CSSOM이라는 구조를 만든다. CSSOM은 스타일 정보를 트리 형태로 정리한 것이다.
  • 눈에 보이는 요소만 처리: 브라우저는 화면에 보이는 요소만 처리한다. 예를 들어 display: none처럼 보이지 않는 요소는 무시해서 렌더링 속도를 조금이라도 높인다.
  • 레이아웃 계산: DOM 트리와 CSSOM 트리를 합쳐서 요소들이 화면에서 어디에 위치해야 할지 계산한다. 예를 들어, 제목은 화면 위쪽에, 버튼은 아래쪽에 위치해야 하는지 등을 결정하는 단계이다.
  • 화면에 그림 그리기(페인팅): 마지막으로, 색과 이미지 같은 실제 스타일을 각 위치에 적용해 화면에 표시한다.

 

#text {
	background-color: red;
	color: white;
}
<!DOCTYPE>
<html>
	<head>
		<link rel="stylesheet" type="text/css" href="./style.css" />
		<title>Hello React!</title>
	</head>
	<body>
		<div style="width: 100%;">
			<div id="text" style="width: 50%;">Hello world!</div>
		</div>
	</body>
</html>

 

위에 코드를 예시로 과정을 보면,

  • HTML을 다운로드한다. 다운로드와 함께 HTML을 분석하기 시작한다.
  • 스타일시트가 포함된 link태그를 발견해 style.css를 다운로드한다.
  • body 태그 하단의 div는 width: 100%이므로 뷰포트(브라우저가 사용자에게 노출하는 영역)로 좌우 100% 너비로 잡는다.
  • 3번 하단의 div는 width: 50%, 즉 부모의 50%를 너비로 잡아야 하므로 전체 영역의 50%를 너비로 잡는다.
  • 2번에서 다운로드한 css에 id="text"에 대한 스타일 정보를 결합한다.
  • 화면에 HTML 정보를 그리기 위한 모든 정보가 준비됐으므로 위 정보를 바탕으로 렌더링을 수행한다.

2.2.2 가상 DOM의 탄생 배경

  • 브라우저가 웹페이지를 렌더링하는 과정은 매우 복잡하고 많은 비용이 발생한다.
  • 따라서 사용자 인터렉션으로 인해 웹페이지가 변경되는 상황을 고려해야 한다.
  • 요소의 크기가 바뀌는 경우 레이아웃이 일어나고 이는 필연적으로 리페인팅 과정이 발생하면서 큰 비용이 발생한다.
  • 또한 DOM 변경이 일어나는 요소가 많은 자식을 가지고 있는 경우 덩달아서 자식 요소까지 바뀌기 때문에 더더욱 큰 비용이 발생한다.
  • 이러한 렌더링 이후 추가적인 렌더링 작업은 SPA에서 더욱 많아진다.
  • 이러한 문제점을 해결하기 위해 탄생한 것이 바로 가상 DOM이다.
  • 리액트는 웹페이지에 표시해야 할 DOM을 메모리제 저장하고, 실제 변경에 대한 준비가 완료되었을 때 실제 브라우저의 DOM에 반영한다.
  • 이렇게 DOM 계산을 브라우저가 아닌 메모리에서 계산하는 과정을 한 번 거치게 된다면 실제로는 여러 번 발생했을 때 렌더링 과정을 최소화할 수 있고 브라우저와 개발자의 부담을 덜 수 있다.
  • 가상 DOM에 대한 한 가지 일반적 오해는 일반적인 DOM을 관리하는 브라우저보다 무조건 빠르다는 사실이다. 이는 사실이 아니며, 가상 DOM방식은 대부분의 상황에서 충분히 빠르다는 것이다.

2.2.3 가상 DOM을 위한 아키텍처, 리액트 파이버 

 

리액트 파이버란?

  • 리액트에서 관리하는 자바스크립트 객체다.
  • 파이버는 파이버 재조정자 (Fiber Reconciler)가 관리한다.
  • 이는 가상DOM과 실제DOM을 비교해 변경 사항을 수집하며, 만약 이 둘 사이에 차이가 있으면 변경에 관련된 정보를 가지고 있는 파이버를 기준으로 화면에 렌더링을 요청한다.
  • React 16v에 등장하였고, 이전까진 diff 알고리즘을 통해 가상DOM과 비교하였다.
  • 과거 사용된 리액트의 조정 알고리즘은 스택 알고리즘으로 이뤄져 있었다. 스택에 렌더링에 필요한 작업들이 쌓이면 이 스택이 빌 때까지 동기적으로 작업이 이루어졌다.
  • 싱글 스레드인 자바스크립트에선 이 동기 작업은 중단될 수 없고, 다른 작업을 수행할 수도 없었다.
  • 이러한 기존 렌더링 스택의 비효율성을 타파하기 위해 리액트에서는 파이버라는 개념이 등장했다.
더보기

개념이 어려워 챗지피티한테 쉬운 설명을 부탁했다.

 

리액트의 "파이버(Fiber)"는 리액트가 화면을 빠르고 효율적으로 업데이트하기 위해 도입한 새로운 구조에요. 복잡한 말 같지만, 쉽게 설명하자면, 화면을 업데이트할 때 리액트가 작업을 "잘게 나누어 효율적으로 처리할 수 있게" 해주는 방식이에요.

 

왜 파이버가 필요할까?

리액트는 화면을 업데이트할 때 컴포넌트를 다시 계산하고 화면에 반영하는 작업을 해요. 그런데, 이 작업이 복잡한 화면에서는 시간이 오래 걸릴 수 있어요. 그래서 사용자들이 앱을 사용하면서 반응이 느려진다거나, 멈추는 것처럼 보일 수 있죠. 예를 들어, 애니메이션이 끊기거나 스크롤이 부드럽지 않은 문제가 생길 수 있어요.

 

파이버가 어떻게 도와줄까?

파이버는 이렇게 느려질 수 있는 작업을 잘게 쪼개서, "조금씩 나눠서" 처리해요. 필요한 만큼만 빠르게 작업하고, 화면이 잘 작동할 수 있게 다음 작업은 잠시 기다리는 거죠.

이 과정을 학교 공부에 비유해 볼게요:

  1. 전체 과제와 파이버의 나눠서 작업하기: 큰 과제가 주어졌을 때, 한꺼번에 끝내기 힘들다면 조금씩 쪼개서 매일 일정 시간 동안 과제를 해나가는 거예요. 파이버는 화면을 업데이트할 때, 복잡한 작업을 "작은 단위로 쪼개서 처리"하는 거랑 비슷해요.
  2. 우선순위가 중요한 작업: 공부를 할 때, 급한 과제는 먼저 하고, 천천히 해도 되는 건 나중에 하죠. 파이버도 마찬가지로 급한 업데이트는 먼저 하고, 덜 급한 건 나중에 해요. 이렇게 하면 사용자가 버튼을 클릭하거나 스크롤할 때, 화면이 더 빠르게 반응할 수 있어요.
  3. 중간에 쉬어가기: 리액트는 파이버를 이용해 작업을 중간에 멈출 수 있어요. 예를 들어, 애니메이션이 끝날 때까지 잠깐 기다렸다가 다시 나머지 작업을 이어가는 거죠. 이렇게 하면 사용자가 화면에서 "멈칫"하는 느낌을 덜 받게 돼요.

요약하자면!

리액트 파이버는 큰 작업을 작은 조각으로 나누어, 급한 것부터 먼저 처리하고, 필요한 순간마다 잠시 멈췄다가 이어갈 수 있게 해줘요. 덕분에 화면 업데이트가 더 부드럽고 빠르게 반응하도록 해주는 역할을 해요!


 

리액트 파이버 구조는 화면을 업데이트할 때 필요한 정보를 효율적으로 관리하고 빠르게 작업을 처리하기 위해 만들어진 요소들이에요. 각 속성들은 파이버 간 관계를 이해하고, 변화한 내용을 트리에 반영하는 데 중요한 역할을 합니다. 하나씩 쉽게 설명해 볼게요.

 

1. tag: 파이버와 요소(element)의 연결

tag는 파이버가 어떤 요소와 연결되는지 알려줘요. 파이버는 화면의 요소들 각각과 연결되어 1:1 관계를 가지며, 어떤 요소를 다루고 있는지 확인할 수 있도록 tag에 초기화됩니다.

 

2. stateNode: 파이버의 실제 참조 정보

stateNode는 파이버가 실제로 어떤 객체를 가리키는지 나타내요. 이를 통해 파이버가 화면의 특정 요소나 컴포넌트와 연결될 수 있죠. 예를 들어, stateNode를 사용해 버튼 클릭 시 버튼 파이버의 위치나 상태를 파악할 수 있어요.

 

3. child, sibling, return: 파이버 간 관계 관리

파이버 간의 관계는 컴포넌트 구조를 효율적으로 탐색하고 처리하기 위해 중요해요.

  • child: 파이버의 첫 번째 자식 요소를 가리켜요. 이걸 통해 파이버가 어떤 자식 구조를 가지고 있는지 알 수 있어요.
  • sibling: 파이버의 형제 요소를 가리켜요. 이렇게 하면 같은 레벨에 있는 요소들끼리 연결될 수 있어요.
  • return: 파이버의 부모 요소를 가리켜요. 즉, 현재 파이버가 상위 컴포넌트와 어떤 관계에 있는지를 파악하게 돼요.

4. index: 파이버의 순서

index는 형제들 사이에서 파이버의 순서를 나타내요. 여러 요소가 있을 때, 그중 어느 위치에 있는지를 파악하기 위해 사용되며, 이를 통해 화면을 업데이트할 때 빠르게 요소를 찾을 수 있어요.

 

5. pendingProps: 아직 처리되지 않은 props

pendingProps는 아직 업데이트되지 않은 새로운 속성(props) 정보예요. 새롭게 변경된 내용이 파이버에 들어오면, 이 pendingProps에 임시로 저장해서 나중에 처리할 수 있도록 하는 역할이에요.

 

6. memoizedProps: 렌더링 완료된 props

memoizedProps는 최신 props 상태를 보관해요. pendingProps로 받은 값을 기준으로 화면이 업데이트되면, 이 값이 memoizedProps에 저장돼요. 이를 통해 화면 업데이트 전후의 props 변화를 관리할 수 있죠.

 

7. updateQueue: 작업을 관리하는 대기열

updateQueue는 상태 업데이트, 콜백 함수, 그리고 DOM 업데이트 같은 작업을 담아두는 곳이에요. 이 대기열을 통해 리액트는 화면에 적용할 여러 작업을 차례대로 관리할 수 있게 돼요.

 

8. memoizedState: 함수 컴포넌트의 훅 목록

memoizedState는 함수 컴포넌트가 가지고 있는 훅(예: useState, useEffect)들을 저장해요. 이를 통해 컴포넌트가 각 훅의 상태를 기억하고, 렌더링될 때 필요한 상태 값을 불러올 수 있어요.

 

9. alternate: 두 개의 트리 중 반대쪽 트리

리액트에는 화면 업데이트를 빠르게 하기 위해 두 개의 트리(현재 트리와 새롭게 변경될 트리)가 있어요. 이 alternate는 현재 파이버의 반대 트리를 가리켜요. 이 방식을 통해 리액트는 업데이트 전후의 두 트리를 비교하면서 효율적으로 화면을 바꿀 수 있어요.

이 속성들이 다 함께 사용되면서, 리액트는 복잡한 화면 업데이트를 효율적으로 관리하고, 변경된 내용만 빠르게 처리해요.

리액트 파이버 트리

  • 리액트 파이버의 작업이 끝나면 리액트는 단순히 포인터만 변경해 workInProgress 트리를 현재 트리로 바꿔버린다. 이를 더블 버퍼링이라고 한다. 
  • 한번에 모든 작업을 마무리해 다 그릴 수 없어 미처 다 그리지 못한 모습이 사용자에게 보여지는 경우가 발생하는데 이를 방지하기 위해 보지이 않는 곳에서 그 다음으로 그려야 할 그림을 미리 그린 다음, 이것이 완성되면 현재 상태를 새로운 그림으로 바꾸는 기법을 의미한다. 
  • 리액트에서는 이 더블 버퍼링 작업을 커밋 단계에서 수행한다.

파이버의 작업 순서

  • 리액트는 beginWork() 함수를 실행해 파이버 작업을 수행하는데, 더 이상 자식이 없는 파이버를 만날 때까지 트리 형식으로 시작된다.
  • 위 작업이 끝나면 그 다음 completeWork() 함수를 실행해 파이버 작업을 완료한다.
  • 형제가 있다면 형제로 넘어간다. 
  • 위 작업이 모두 끝나면 return으로 돌아가 자신의 작업이 완료됐음을 알린다.

2.2.4 파이버와 가상 DOM

  • 파이버는 1:1로 리액트 컴포넌트에 대한 정보를 가지고 있다.
  • 파이버는 리액트 아키텍처 내부에서 비동기로 이뤄진다.
  • 복잡한 과정은 가상에서 즉, 메모리상에서 먼저 수행해서 최종적인 결과물만 실제 브라우저 DOM에 적용한다.

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

2.3.1 클래스 컴포넌트

import React from "react";

//prop 타입을 선언한다.
interface SampleProps {
    required?: boolean
    text: string
}

//state 타입을 선언한다.
interface SampleState {
    count: Number
    isLimited?: boolean
}

//Component에 제네릭으로 props, state를 순서대로 넣어준다.
class SampleComponent extends React.Component<SampleProps, SampleState> {
    //constructor에서 props를 넘겨주고, state의 기본값을 설정한다.
    private constructor(props: SampleProps) {
        super(props)
        this.state = {
            count: 0,
            isLimited: false,
        }
    }

    //render 내부에서 쓰일 함수를 선언한다.
    private handleClick = () => {
        const newValue = this.state.count+1
        this.setState({ count: newValue, isLimited: newValue >= 10})
    }

    //render에서 이 컴포넌트가 렌더링할 내용을 정의한다.
    public render () {
        //props와 state 값을 this, 즉 해당 클래스에서 꺼낸다.
        const {
            props: { required, text },
            state: { count, isLimited },
        } = this

        return (
            <h2>
                Sample Component
                <div>{required ? "필수" : "필수아님"}</div>
                <div>문자: {text}</div>
                <div>count: {count}</div>
                <button onClick={this.handleClick} disabled={isLimited}>증가</button>
            </h2>
        )
    }
}

 

클래스 컴포넌트를 만들려면 클래스를 선언한고 만들고 싶은 컴포넌트를 extends한다.