frontend

React19 Support for Document Metadata / stylesheets / async scripts / preloading resources

하리하링웹 2024. 6. 4. 21:02

Support for Document Metadata / stylesheets / async scripts / preloading resources

Support for Document Metadata

<head> section에 들어가는 <title>, <link>, <meta> 와 같은 메타 태그들을 컴포넌트단에서 렌더링 가능하도록 변경

→ 과거에는 수동으로 삽입해주거나 react-helmet, next-seo와 같은 seo 라이브러리들을 사용하여 넣어줘야 했지만 이를 개선

react에서는 이런 메타 태그들을 자동으로 <head> 섹션으로 올려주며 이는 ssr, server-component, csr 모든곳에서 잘 동작하게 된다.

주의사항

일반적으로 컴포넌트 하나하나 직접 메타 태그를 작성하는 것 보다는 라이브러리를 통하여 기본 설정, 오버라이딩, 경로에 따른 다이나믹 핸들링 등의 기능을 사용하는 것이 편하기 때문에 이번에 추가된 기능은 단순하게 기존에 불가능했던 부분들을 보완해주는 역할만 한다고 생각하는게 좋을듯

Support for stylesheets

기존에는 외부 스타일 시트 <link rel="stylesheet" href="..."> 혹은 인라인 스타일 <style>...</style> 등의 우선 순위 관리가 힘들었기에 이를 컴포넌트와 분리하여 관리하거나 스타일 라이브러리를 사용했음

문제)

  • 스타일 시트를 수동으로 <head>에 적절한 순서로 삽입해야함
  • 인라인 스타일과 삽입한 스타일간의 우선순위 충돌
  • 중복 스타일 로드
  • 어려운 동적인 스타일 로드 + 렌더링 관리
  • SSR 관리 ( 필요한 스타일 미리 로드 )

기존 해결 방안)

  • tailwind css의 utility class 사용을 통한 css 중복 제거
  • styled component를 통한 컴포넌트 별 스타일 분리 관리

react 19에서는 이를 해결하기 위해 스타일 시트의 우선순위를 직접 지정할 수 있도록 했으며 외부 스타일 시트의 경우 해당 콘텐츠의 렌더링 전에 스타일 시트가 먼저 로드되도록 보장하는 기능을 추가하였다.

function ComponentOne() {
  return (
    <Suspense fallback="loading...">
      <link rel="stylesheet" href="foo" precedence="default" />
      <link rel="stylesheet" href="bar" precedence="high" />
      <article class="foo-class bar-class">
        {...}
      </article>
    </Suspense>
  );
}

function ComponentTwo() {
  return (
    <div>
      <p>{...}</p>
      <link rel="stylesheet" href="baz" precedence="default" />  <-- foo와 bar 사이에 삽입됨
    </div>
  );
}
  • react는 서버 사이드 렌더링의 경우 스타일 시트를 <head> 에 포함시켜 브라우저가 해당 스타일 시트를 로드할 때 까지 페인트 작업을 하지 않도록 보장해주며 이미 스트리밍이 시작된 상태에서 스타일 시트를 발견했다면 이 스타일 시트 역시 <head>에 삽입한 뒤 먼저 처리해준다.
  • 클라이언트 사이드 렌더링에서도 새로 렌더링된 스타일 시트가 로드될 때까지 새 렌더링을 commit 하지 않는다.
  • 여러 위치에서 스타일 시트를 가진 컴포넌트를 렌더링해도 React는 이 스타일 시트를 한 번만 로드한다.
function App() {
  return <>
    <ComponentOne />
    ...
    <ComponentOne /> // won't lead to a duplicate stylesheet link in the DOM
  </>
}

Support for async scripts

기존 react에서는 **일반 script <script src="...">**와 지연 script <script defer="" src="..."> 는 html 순서대로 로드되어 이를 html 트리 최하단에서 렌더링하는 것이 쉽지 않았음 게다가 비동기 scripts <script async="" src="..."> 는 먼저 로드된 스크립트가 실행되기 때문에 실행 순서를 보장할수도 없음

useEffect에서 태그를 동적으로 삽입, 로드하거나 react-helmet 과 같은 라이브러리를 사용하여 동적으로 스크립트를 관리해야했음

react 19에서는 비동기 스크립트의 편의성을 높여 컴포넌트 안에서 비동기 스크립트를 직접 위치시킬 수 있게 만들었으며 중복 스크립트 실행도 방지해주도록 개선

function MyComponent() {
  return (
    <div>
      <script async={true} src="..." />
      Hello World
    </div>
  );
}

function App() {
  return (
    <html>
      <body>
        <MyComponent />
        ...
        <MyComponent /> {/* DOM에 중복 스크립트가 생성되지 않음 */}
      </body>
    </html>
  );
}

SSR의 경우에는 비동기 스크립트가 <head> 에 포함되어 스타일 시트, 폰트, 이미지와 같이 페인트를 차단할 수 있는 리소스 뒤에 알아서 배치하여 초기 렌더링 시간을 향상시킬 수 있게 만들어줌

Support for preloading resources

초기 로드를 늦출 수 있는 리소스를 프리로드 할 수 있게 하는 새 API가 추가되었음

import { prefetchDNS, preconnect, preload, preinit } from 'react-dom';

function MyComponent() {
  preinit('https://.../path/to/some/script.js', { as: 'script' }); // 스크립트를 적극적으로 로드하고 실행
  preload('https://.../path/to/font.woff', { as: 'font' }); // 폰트를 프리로드
  preload('https://.../path/to/stylesheet.css', { as: 'style' }); // 스타일시트를 프리로드
  prefetchDNS('https://...'); // 실제로 해당 호스트에서 요청이 발생하지 않을 때 DNS를 미리 가져옴
  preconnect('https://...'); // 요청이 발생할 가능성이 있지만 무엇을 요청할지 확실하지 않을 때 미리 연결
}

위 코드는 아래와 같은 결과를 생성해줌

<html>
  <head>
    <!-- 링크/스크립트는 호출 순서가 아닌 초기 로딩에 유용한 순서로 우선 순위가 매겨짐 -->
    <link rel="prefetch-dns" href="https://...">
    <link rel="preconnect" href="https://...">
    <link rel="preload" as="font" href="https://.../path/to/font.woff">
    <link rel="preload" as="style" href="https://.../path/to/stylesheet.css">
    <script async src="https://.../path/to/some/script.js"></script>
  </head>
  <body>
    ...
  </body>
</html>

이를 통해 초기 렌더링을 늦출 수 있는 요소들을 우선순위 혹은 설정에 따라 미리 가져오거나 최적화하여 가져와 초기 렌더링을 향상시킬 수 있음

주요 기능

  1. preinit:
    • 초기 로드 시 중요한 스크립트를 미리 로드하여 페이지 로드 시간을 단축
  2. preload:
    • 폰트, 스타일시트, 이미지 등 다양한 리소스를 미리 로드
  3. prefetchDNS:
    • 특정 호스트에서 요청이 발생하지 않을 경우에도 DNS fetch를 미리 실행하여 네트워크 지연을 감소
  4. preconnect:
    • 특정 호스트에 대해 요청이 발생할 가능성이 있지만 무엇을 요청할지 확실하지 않을 때, 미리 연결을 설정하여 네트워크 지연을 최소화

Compatibility with third-party scripts and extensions

서드파티 스크립트 및 브라우저 확장 프로그램과의 호환성을 개선하기 위해 하이드레이션(hydration)을 개선

  1. hydration 불일치 개선
  • 클라이언트에서 렌더링된 요소가 서버에서 전달된 HTML의 요소와 일치하지 않으면, 해당 콘텐츠 클라이언트에서 리렌더링
  • 이전에는 서드파티 스크립트나 브라우저 확장 프로그램이 삽입한 요소 때문에 불일치 오류가 발생했고, 이로인해 전체 클라이언트 리렌더링
  1. 태그 건너뛰기
  • **<head>**와 **<body>**에 예상치 못한 태그가 있더라도 이를 무시하고 건너뛰어, 불일치 오류를 방지, 이는 third-party 스크립트나 브라우저 확장 프로그램이 삽입한 태그가 hydration 과정에서 문제를 일으키지 않게 만들어줌
  1. 스타일시트 유지
  • React가 전체 문서를 리렌더링해야 할 경우에도, third-party 스크립트나 브라우저 확장 프로그램이 삽입한 스타일 시트를 그대로 유지 → 일시적으로 스타일이 깨지거나 깜빡거리는 등의 문제 방지

 

Better error reporting

에러 바운더리 개선

기존에는 1. 기존에러 2. restore 실패 후 에러 3. console.error를 에러 발생 위치 및 콜스택 출력 에러 로그 즉 한 개의 에러에 총 3개의 로그가 찍혔음

이제 이를 모두 포함한 한 개의 로그만 남김

기존 onRecoverableError 를 보완하기 위해 새로운 옵션을 추가 (onRecoverableError 는 react 18에서 createRoot에 추가된 콜백 옵션)

  • onCaughtError: Error Boundary에서 에러가 잡혔을 때 호출
  • onUncaughtError: 에러가 발생하고 Error Boundary에 의해 잡히지 않았을 때 호출
  • onRecoverableError: 에러가 발생하고 자동으로 복구되었을 때 호출

 

 

Support for Custom Elements

기존에는 react가 인식하지 못한 props를 attribute로 인식해 처리하여 사용하기 어려웠던 custom element의 사용성 개선

  • property: 자바스크립트 객체의 속성, html의 상태를 나타내거나 조작하는데에 사용 (동적) ex) document.querySelector('input').value = "new value”는 property 수정 예시
  • attribute :html 태그에 명시적으로 작성되는 값 (초기 값) ex) <input type="text" value="initial">에서 value

react 19에서는 아래 규칙에 따라 custom element가 파싱

SSR

  1. 문자열, 숫자, 또는 값이 true인 원시 타입의 props
    • 이러한 값들은 HTML 태그의 attribute로 렌더링됨
    • ex) "hello", 123, true
  2. 객체, 심볼, 함수, 또는 값이 false인 원시 타입이 아닌 props
    • 이러한 값들은 HTML 태그에서 생략
    • 예: {}, () => {}, false
function MyComponent() {
  return (
    <div>
      <my-custom-element
        stringProp="Hello" // attribute
        numberProp={123}   // attribute
        booleanProp={true} // attribute
        objectProp={{ key: 'value' }}  // 생략
        functionProp={() => {}}  // 생략
        falseProp={false}        // 생략
      />
    </div>
  );
}

SSR 결과

<my-custom-element stringProp="Hello" numberProp="123" booleanProp></my-custom-element>

CSR

  1. custom element의 property와 일치하는 props는 property로 할당.
  2. 일치하지 않는 props는 attribute로 할당.
class MyCustomElement extends HTMLElement {
  set stringProp(value) 
  set numberProp(value) 
  set booleanProp(value) {
}

customElements.define('my-custom-element', MyCustomElement);

function MyComponent() {
  return (
    <div>
      <my-custom-element
        stringProp="Hello" // property
        numberProp={123} // property
        booleanProp={true} // property
        objectProp={{ key: 'value' }} // attribute
        functionProp={() => {}} // attribute
        falseProp={false} // attribute
      />
    </div>
  );
}