[ React ] 모던 리액트 DEEP DIVE - CH 2.2, 2.3 + JSX

·2024년 3월 12일

2.1 JSX

2.1.1 JSXElement

JSX 구성의 기본 요소이며, html의 요소와 비슷한 역할을 한다.

2.1.2 JSXElementName

JSXElement의 요소 이름으로 쓸 수 있는 것

  • JSXIdentifier : JSX 내부에서 사용할 수 있는 식별자, 자바스크립트 식별자 규칙과 동일하다. 즉 <$></$> <_></_>도 가능하지만, 숫자로 시작하거나 &와 _ 외의 특수문자로는 시작할 수 없다.
  • JSXNamespacedName: :을 통해 서로 다른 식별자를 이어주는 것도 하나의 식별자로 취급한다. 다만, 한 개까지만 묶을 수 있다.
<foo:bar></foo:bar>
  • JSXMemberExpression: .을 통해 서로 다른 식별자를 이어주는 것도 하나의 식별자로 취급한다. 또한 .을 여러 개 이어서 하는 것도 가능하다.
    :와는 섞어서 쓸 수 없다.
<foo.bar.baz></foo.bar.baz>

2.1.3 JSXAttributes

JSXElement에 부여할 수 있는 속성을 의미한다.

  • JSXSpreadAttributes: 자바스크립트의 전개 연산자와 동일한 역할을 한다.
  • JSXAttributeValue: 속성의 키에 할당할 수 있는 값으로, 자바스크립트와 거의 동일하다.
    -> "", '' : 자바스크립트의 문자열과 동일
    {}: 자바스크립트에서 값을 할당할 때 쓰는 표현식

2.1.4 JSXChildren

JSXChildren을 이루는 기본 단위이다.

2.2 가상 DOM과 리액트 파이버

2.2.1 DOM이란?

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

브라우저가 웹사이트 접근 요청을 받을 때의 과정

  1. 브라우저가 사용자가 요청한 주소를 방문해 HTML을 다운로드
  2. 브라우저의 렌더링 엔진은 HTML을 파싱해 DOM 노드로 구성된 DOM 트리를 만든다.
  3. 2번 과정에서 CSS 파일을 만나면 CSS 파일도 다운로드한다.
  4. 브라우저의 렌더링 엔진은 CSS도 파싱해 CSS 노드로 구성된 CSSOM 트리를 만든다.
  5. 2번에서 만든 DOM 노드를 순회하는데, 사용자의 눈에 보이는 노드만 방문한다. ( display: none과 같이 사용자 화면에 보이지 않는 요소는 방문해서 작업하지 않는다.)
  6. 5번에서 제외된 눈에 보이는 노드를 대상으로 해당 노드에 대한 cssom 정보를 찾고 css 정보를 노드에 적용한다.
  • 레이아웃 : 각 노드가 브라우저 화면의 어느 좌표에 정확히 나타나야 하는지 계산하는 과정
  • 페인팅: 레이아웃 단계를 거친 노드에 색과 같은 실제 유효한 모습을 그리는 과정

2.2.2 가상 DOM의 탄생 배경

DOM 변경이 일어나는 요소가 많은 자식 요소를 가지고 있는 경우에는 하위 자식 요소도 덩달아 변경되어야 하기 때문에, 더 많은 비용이 지불된다.
또한, 하나의 인터랙션으로 인해 페이지 내부의 DOM의 여러가지가 변경되는 시나리오는 현재 웹페이지에서 흔히 볼 수 있다.
이 경우 개발자는 인터랙션에 모든 DOM의 변경보다는 결과적으로 만들어지는 DOM의 결과물 하나만 알고 싶어 한다.

따라서, 이러한 문제점을 해결하기 위해 탄생한 것이 가상 DOM이다

2.2.2 가상 DOM

실제 브라우저의 DOM이 아닌, 리액트가 관리하는 가상의 DOM을 의미
가상 DOM이란 웹페이지가 표시해야 할 DOM을 일단 메모리에 저장하고, 실제 변경에 대한 준비가 완료되었을 때 실제 브라우저의 DOM에 반영한다.

2.2.3 리액트 파이버

리액트 파이버는 리액트에서 관리하는 평범한 자바스크립트 객체이다.
가상 DOM과 실제 DOM을 비교해 변경 사항을 수집하며, 차이가 있으면 변경에 관련된 정보를 가지고 있는 파이버를 기준으로 화면에 렌더링을 요청하는 역할을 한다.

파이버는 애니메이션, 레이아웃, 인터랙션의 결과물을 만드는 반응성 문제를 해결하기 위해 나타났다.
따라서, 파이버는 다음과 같은 일을 할 수 있다.

  • 작업을 작은 단위로 분할하고, 쪼갠 다음 우선순위를 매긴다
  • 이러한 작업을 일시 중지하고 나중에 다시 시작할 수 있다.
  • 이전에 했던 작업을 다시 재사용하거나 필요하지 않은 경우에는 폐기할 수 있다.

과거의 리액트는 스택 알고리즘으로 이뤄어져 있는데, 이는 모든 작업들이 동기적으로 이루어짐을 의미한다.
이러한 비효율성을 해결하기 위해 파이어의 개념이 도입된다.

파이버의 구현

파이버는 하나의 작업 단위로 구성되어 있다.
리액트는 이러한 작업 단위를 하나씩 처리하고, finishWork() 라는 작업으로 마무리한다. 이러한 작업을 커밋해서 실제 브라우저 DOM에 가시적인 변경 사항을 만들어낸다.

  1. 랜더 단계 : 사용자에게 노출되지 않는 모든 비동기 작업 수행, 파이버의 작업, 우선순위를 지정하거나 중지시키거나 버리는 등의 작업
  2. 커밋 단계 : DOM에 실제 변경 사항을 반영하기 위한 작업, commitWork() 가 실행됨

파이버 트리

workInProgress 트리: 작업 중인 상태를 나타내는 트리,
더블 버퍼링: 리액트 파이버 작업이 끝나면, 리액트는 포인터만 변경해 workInProgres 트리를 현재 트리로 바꿈
이러한 더블 버퍼링을 위해 트리가 두 개 존재하며, 커밋 단계에서 수행된다.

  1. current 기준으로 모든 작업이 시작
  2. 업데이트가 발생하면 파이버는 workInProgress 트리 빌드
  3. 빌드 이후 다음 렌더링에 이 트리 사용
  4. 렌더링되어 반영되면 current가 workInProgress로 변경됨

    2.2.4 파이버와 가상 DOM

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

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

    2.3.1 클래스형 컴포넌트

class SampleComponent extends React.Component {
  render() {
    return <h1>Hi</h1>
  }
}

기본적인 클래스형 컴포넌트는 클래스를 선언하고, extends로 만들고 싶은 컴포넌트를 extends 한다.

생명주기 메서드

  • render()

    리액트 클래스형 컴포넌트의 유일한 필수 값으로 항상 쓰이며, 컴포너트가 UI를 렌더링하기 위해서 쓰인다. render() 함수는 항상 순수해야 하며, 부수 효과가 없어야 한다. 즉 항상 같은 결과물을 반환해야 한다.

  • componentDidMount()

    컴포넌트가 마운트되고 즉시 실행되며 state값 변경이 가능하다.
    ui 업데이트 전에 실행되어 사용자가 변경되는 것을 눈치챌 수 없다.

  • componentDidUpdate()

    컴포넌트 업데이트가 일어난 이후 바로 실행되며, state나 props의 변화에 따라 DOM을 업데이트하는 등에 쓰인다.
    하지만, state가 계속해서 호출될 수 있어 주의해야 한다.

  • componentWillUnmount()

    컴포넌트가 언마운트되거나 더 이상사용되지 않기 직전에 호ㅜㄹ되며, 클린업 함수 호출하기 위한 최적의 위치이다. state업데이트는 불가능하다.

  • shouldComponentUpdate()

    state나 props의 변경으로 리액트 컴포너트가 다시 리렌더링되는 것을 막고 싶다면 사용할 수 있다.
    하지만, 이는 특정한 성능 최적화 상황에서만 고려하는 게 좋다.

  • state getDrivedStateFormProps()

    render()를 호출하기 직전에 호출된다. 또한 static으로 선언되어 있어 this에 접근할 수 없다.

  • getSnapShotBeforeUpdate()

    DOM이 업데이트되기 지겆ㄴ에 호출된다.
    렌더링 되기 전 윈도우 크기를 조절하거나 스크롤 위치를 조정하는 등의 작업 처리에 유용하다.

  • getDerivedStateFromError()

    에러 상황에서 실행되는 메서드이며, error를 인수로 받는다.
    이때 error는 하위 컴포넌트에서 발생한 에러를 말하며, 반드시 state값을 반환해야 한다.
    하위 컴포넌트에서 에러가 발생했을 경우에 어떻게 자식 리액트 컴포넌트를 렌더링할지 결정하는 용도로 제공되는 메서드이기 때문에 반드시 미리 정의해 둔 state값을 반환해야 한다.
    또한 렌더링 과정에서 호출되는 메서드이기 때문에 부수 효과를 발생시켜서는 안된다.
    ( 에러에 따른 상태 외에 모든 작업 )

  • componentDidCatch()

    자식 컴포넌트에서 에러가 발생했을 때 실행되며, 에러를 getDerivedStateFromError() 에서 잡고, state를 결정한 이후에 실행된다.
    두 개의 인수를 받는데, 첫 인수는 getDerivedStateFromError()에서 동일한 error, 또한 두번째 인수는 어떤 컴포넌트가 에러를 발생시켰는지 정보를 가지고 있는 info이다.

클래스형 컴포넌트의 한계

데이터 흐름 추적이 힘들다
애플리케이션 내부 로직의 재사용이 어렵다.
기능이 많아질수록 컴포넌트의 크기가 커진다.

2.3.2 함수형 컴포넌트

함수형 vs 클래스형

생명주기의 부재

클래스형 컴포넌트는 render 메서드가 있는 React.Component를 상속받아 구현하는 자바스ㅡ립트 클래스이기 때문에, 함수형 컴포넌트에서는 (props를 받아 리액트 요소 반환) 생명주기 메서드를 사용할 수 없다.

렌더링된 값

함수형 컴포넌트는 렌더링된 값을 고정하고, 클래스형 컴포넌트는 그렇지 못한다.

export function Functional(props){
  const showMessage = () => {
    alert('Hello' + props.user)
  }

  const handleClick = () => {
    setTimeout(showMessage, 3000)
  }
  return <button onClick={handleClick}>Follow</button>
}

  export class Class extends React.Component<{}>{
    private showMessage = () => {
      alert('Hello' + this.props.user)
  }

	private handleClick = () => {
   	 setTimeout(showMessage, 3000)
 	 }
	 public render() {
    return <button onClick={handleClick}>Follow</button>
	}
}

이 경우, 3초 사이에 props를 변경한다면,
Class형의 경우 3초 뒤에 변겨된 props를 기준으로 메세지가 뜨고,
Funciton의 경우 클릭했던 시점의 값을 기준으로 메세지가 뜬다.

클래스형 컴포넌트는 props의 값을 항상 this로부터 가져온다.
이때 props는 외부에서 변경되지 않는 이상 불변 값이지만, this가 가리키는 객체, 즉 컴포넌트의 인스턴스의 멤버는 변경 가능한 값이다.
따라서, props를 변경해 컴포넌트가 다시 렌더링됐다는 것은 this.props의 값이 변경되었다는 것이다.

profile
new blog: https://hae0-02ni.tistory.com/

0개의 댓글