Next(또는 모든 SSR 프레임워크)와 함께 웹 구성 요소 사용

내에서 이전 게시물 우리는 아름답고 접근 가능하며 아마도 예기치 않게 웹 컴포넌트. 이는 모든 JavaScript 프레임워크와 함께 사용할 수 있음을 의미합니다. React의 웹 컴포넌트 상호 운용성은 현재 이상적이지 않지만, 해결 방법이 있습니다.

그러나 웹 구성 요소의 심각한 단점 중 하나는 현재 SSR(서버 측 렌더링)에 대한 지원이 부족하다는 것입니다. 작업 중인 DSD(Declarative Shadow DOM)라는 것이 있지만 현재 지원은 매우 미미하며 실제로 DSD에 대한 특수 마크업을 내보내려면 웹 서버의 승인이 필요합니다. 현재 작업이 진행 중입니다. 다음 .js 내가 보기를 고대하고 있다는 것. 그러나 이 게시물에서는 Next.js와 같은 SSR 프레임워크에서 웹 구성 요소를 관리하는 방법을 살펴보겠습니다. 오늘.

우리는 사소하지 않은 양의 수동 작업을 수행하고 약간 그 과정에서 페이지의 시작 성능을 손상시킵니다. 그런 다음 이러한 성능 비용을 최소화하는 방법을 살펴보겠습니다. 그러나 실수하지 마십시오. 이 솔루션에는 장단점이 있으므로 다른 방법을 기대하지 마십시오. 항상 측정하고 프로파일링하십시오.

문제

본격적으로 들어가기 전에 잠시 시간을 내어 실제로 문제를 설명하겠습니다. 웹 구성 요소가 서버 측 렌더링에서 제대로 작동하지 않는 이유는 무엇입니까?

Next.js와 같은 애플리케이션 프레임워크는 React 코드를 가져와서 API를 통해 실행하여 본질적으로 "문자열화"합니다. 즉, 구성 요소를 일반 HTML로 변환합니다. 따라서 React 구성 요소 트리는 웹 앱을 호스팅하는 서버에서 렌더링되고 해당 HTML은 나머지 웹 앱의 HTML 문서와 함께 사용자의 브라우저로 전송됩니다. 이 HTML과 함께 몇 가지 모든 React 구성 요소에 대한 코드와 함께 React를 로드하는 태그. 브라우저가 이러한 작업을 처리할 때 태그를 사용하면 React는 구성 요소 트리를 다시 렌더링하고 전송된 SSR의 HTML과 일치시킵니다. 이 시점에서 모든 효과가 실행되기 시작하고 이벤트 핸들러가 연결되며 상태는 실제로... 상태를 포함합니다. 이 시점에서 웹 앱은 대화형. 클라이언트에서 구성 요소 트리를 다시 처리하고 모든 것을 연결하는 프로세스를 호출합니다. 수화.

그렇다면 이것이 웹 구성 요소와 어떤 관련이 있습니까? 글쎄, 당신이 무언가를 렌더링 할 때 같은 신발 끈을 말하십시오. 우리가 방문한 구성 요소 마지막으로:


   General 
   Custom 
   Advanced 
   Disabled 

  This is the general tab panel.
  This is the custom tab panel.
  This is the advanced tab panel.
  This is a disabled tab panel.

… 반응(또는 정직하게 어떤 JavaScript 프레임워크)는 해당 태그를 보고 단순히 전달합니다. React(또는 Svelte, Solid)는 이러한 태그를 멋진 형식의 탭으로 바꾸는 책임이 없습니다. 해당 코드는 해당 웹 구성 요소를 정의하는 코드 내부에 숨겨져 있습니다. 우리의 경우 해당 코드는 Shoelace 라이브러리에 있지만 코드는 어디에나 있을 수 있습니다. 중요한 것은 코드가 실행될 때.

일반적으로 이러한 웹 구성 요소를 등록하는 코드는 JavaScript를 통해 응용 프로그램의 일반 코드로 가져옵니다. import. 즉, 이 코드는 JavaScript 번들에 포함되어 수화 중에 실행됩니다. 즉, 사용자가 SSR의 HTML을 처음 보고 수화 현상이 발생하는 사이에 이러한 탭(또는 해당 문제에 대한 모든 웹 구성 요소)이 올바른 콘텐츠를 렌더링하지 않습니다. . 그런 다음 수분 공급이 발생하면 적절한 콘텐츠가 표시되어 이러한 웹 구성 요소 주변의 콘텐츠가 이동하고 적절한 형식의 콘텐츠에 맞을 수 있습니다. 이것은 스타일이 지정되지 않은 콘텐츠의 플래시, 또는 FOUC. 이론상으로는 그 사이에 마크업을 붙일 수 있습니다. 완성된 출력과 일치하도록 태그를 추가하지만 실제로는 거의 불가능합니다. 특히 Shoelace와 같은 타사 구성 요소 라이브러리의 경우에는 더욱 그렇습니다.

웹 구성 요소 등록 코드 이동

따라서 문제는 웹 구성 요소가 필요한 작업을 수행하도록 하는 코드가 수화가 발생할 때까지 실제로 실행되지 않는다는 것입니다. 이 게시물에서는 해당 코드를 더 빨리 실행하는 방법을 살펴보겠습니다. 즉시, 사실. 웹 구성 요소 코드를 사용자 지정 번들로 묶고 문서에 직접 스크립트를 수동으로 추가하는 방법을 살펴보겠습니다. 따라서 즉시 실행되고 실행될 때까지 문서의 나머지 부분을 차단합니다. 이것은 일반적으로 끔찍한 일입니다. 서버 측 렌더링의 요점은 지원 JavaScript가 처리될 때까지 페이지가 처리되지 않도록 차단합니다. 그러나 일단 완료되면 문서가 처음에 서버에서 HTML을 렌더링할 때 웹 구성 요소가 등록되고 올바른 콘텐츠를 즉시 동기식으로 내보냅니다.

우리의 경우, 우리는 다만 차단 스크립트에서 웹 구성 요소 등록 코드를 실행하려고 합니다. 이 코드는 크지 않으며 후속 방문에 도움이 되도록 일부 캐시 헤더를 추가하여 성능 저하를 크게 줄이려고 합니다. 이것은 완벽한 솔루션이 아닙니다. 사용자가 처음으로 페이지를 탐색하면 해당 스크립트 파일이 로드되는 동안 항상 차단됩니다. 후속 방문은 캐시에 잘 저장되지만 이 절충점은 그렇지 않을 수 있습니다 당신을 위해 실현 가능합니다 - 전자 상거래, 누구? 어쨌든 프로필을 작성하고 측정하고 앱에 대한 올바른 결정을 내리십시오. 게다가 미래에는 Next.js가 DSD와 웹 컴포넌트를 완벽하게 지원할 가능성이 있습니다.

시작하기

우리가 볼 모든 코드는 이 GitHub 레포Vercel과 함께 여기에 배포. 웹 앱은 수분 공급 시 색상과 내용을 변경하는 텍스트와 함께 일부 신발끈 구성 요소를 렌더링합니다. 신발끈 구성 요소가 이미 제대로 렌더링된 상태에서 텍스트가 "Hydrated"로 변경되는 것을 볼 수 있어야 합니다.

사용자 정의 번들 웹 구성 요소 코드

첫 번째 단계는 모든 웹 구성 요소 정의를 가져오는 단일 JavaScript 모듈을 만드는 것입니다. 내가 사용하는 신발끈 구성 요소의 경우 내 코드는 다음과 같습니다.

import { setDefaultAnimation } from "@shoelace-style/shoelace/dist/utilities/animation-registry";

import "@shoelace-style/shoelace/dist/components/tab/tab.js";
import "@shoelace-style/shoelace/dist/components/tab-panel/tab-panel.js";
import "@shoelace-style/shoelace/dist/components/tab-group/tab-group.js";

import "@shoelace-style/shoelace/dist/components/dialog/dialog.js";

setDefaultAnimation("dialog.show", {
  keyframes: [
    { opacity: 0, transform: "translate3d(0px, -20px, 0px)" },
    { opacity: 1, transform: "translate3d(0px, 0px, 0px)" },
  ],
  options: { duration: 250, easing: "cubic-bezier(0.785, 0.135, 0.150, 0.860)" },
});
setDefaultAnimation("dialog.hide", {
  keyframes: [
    { opacity: 1, transform: "translate3d(0px, 0px, 0px)" },
    { opacity: 0, transform: "translate3d(0px, 20px, 0px)" },
  ],
  options: { duration: 250, easing: "cubic-bezier(0.785, 0.135, 0.150, 0.860)" },
});

에 대한 정의를 로드합니다. 구성 요소를 만들고 대화 상자의 일부 기본 애니메이션을 재정의합니다. 충분히 간단합니다. 그러나 여기서 흥미로운 부분은 이 코드를 우리 애플리케이션에 가져오는 것입니다. 우리 간단히 import 이 모듈. 그렇게 하면 일반 JavaScript 번들에 번들되어 수화 중에 실행됩니다. 이것은 우리가 피하려고 하는 FOUC를 일으킬 것입니다.

Next.js에는 사용자 지정 번들 항목에 대한 많은 webpack 후크가 있지만 다음을 사용합니다. 물다 대신에. 먼저 다음을 사용하여 설치하십시오. npm i vite 그런 다음 vite.config.js 파일. 내 모습은 다음과 같습니다.

import { defineConfig } from "vite";
import path from "path";

export default defineConfig({
  build: {
    outDir: path.join(__dirname, "./shoelace-dir"),
    lib: {
      name: "shoelace",
      entry: "./src/shoelace-bundle.js",
      formats: ["umd"],
      fileName: () => "shoelace-bundle.js",
    },
    rollupOptions: {
      output: {
        entryFileNames: `[name]-[hash].js`,
      },
    },
  },
});

그러면 웹 구성 요소 정의가 포함된 번들 파일이 빌드됩니다. shoelace-dir 폴더. 로 옮기자. public Next.js가 제공할 수 있도록 폴더를 만듭니다. 또한 파일 끝에 해시를 사용하여 파일의 정확한 이름을 추적해야 합니다. 다음은 파일을 이동하고 번들 파일의 이름과 함께 간단한 상수를 내보내는 JavaScript 모듈을 작성하는 Node 스크립트입니다(이는 곧 유용할 것입니다).

const fs = require("fs");
const path = require("path");

const shoelaceOutputPath = path.join(process.cwd(), "shoelace-dir");
const publicShoelacePath = path.join(process.cwd(), "public", "shoelace");

const files = fs.readdirSync(shoelaceOutputPath);

const shoelaceBundleFile = files.find(name => /^shoelace-bundle/.test(name));

fs.rmSync(publicShoelacePath, { force: true, recursive: true });

fs.mkdirSync(publicShoelacePath, { recursive: true });
fs.renameSync(path.join(shoelaceOutputPath, shoelaceBundleFile), path.join(publicShoelacePath, shoelaceBundleFile));
fs.rmSync(shoelaceOutputPath, { force: true, recursive: true });

fs.writeFileSync(path.join(process.cwd(), "util", "shoelace-bundle-info.js"), `export const shoelacePath = "/shoelace/${shoelaceBundleFile}";`);

다음은 컴패니언 npm 스크립트입니다.

"bundle-shoelace": "vite build && node util/process-shoelace-bundle",

작동해야 합니다. 나를 위한, util/shoelace-bundle-info.js 이제 존재하며 다음과 같이 보입니다.

export const shoelacePath = "/shoelace/shoelace-bundle-a6f19317.js";

스크립트 로드

Next.js로 가자 _document.js 파일을 만들고 웹 구성 요소 번들 파일의 이름을 가져옵니다.

import { shoelacePath } from "../util/shoelace-bundle-info";

그런 다음 수동으로 렌더링합니다. ~에있는 태그 . 여기 내 전체가 무엇입니까 _document.js 파일은 다음과 같습니다

import { Html, Head, Main, NextScript } from "next/document";
import { shoelacePath } from "../util/shoelace-bundle-info";

export default function Document() {
  return (
    
      
        
      
      
        
); }

그리고 그것은 작동해야합니다! 신발끈 등록은 차단 스크립트에 로드되며 페이지가 초기 HTML을 처리하는 즉시 사용할 수 있습니다.

성능 향상

그대로 둘 수 있지만 Shoelace 번들에 대한 캐싱을 추가해 보겠습니다. Next.js 구성 파일에 다음 항목을 추가하여 이러한 신발끈 번들을 캐시 가능하게 만들도록 Next.js에 지시합니다.

async headers() {
  return [
    {
      source: "/shoelace/shoelace-bundle-:hash.js",
      headers: [
        {
          key: "Cache-Control",
          value: "public,max-age=31536000,immutable",
        },
      ],
    },
  ];
}

이제 우리 사이트에 대한 후속 탐색에서 Shoelace 번들 캐싱이 멋지게 보입니다!

DevTools 소스 패널이 열리고 로드된 신발끈 번들을 보여줍니다.
Next(또는 모든 SSR 프레임워크)와 함께 웹 구성 요소 사용

신발끈 번들이 변경되면 파일 이름이 변경됩니다( :hash 위의 소스 속성에서 일부), 브라우저는 해당 파일이 캐시되지 않았음을 발견하고 단순히 네트워크에서 새로 요청합니다.

최대 포장

이것은 많은 수동 작업처럼 보일 수 있습니다. 그리고 그랬다. 불행히도 웹 구성 요소는 서버 측 렌더링에 대해 더 나은 기본 지원을 제공하지 않습니다.

그러나 우리는 그들이 제공하는 이점을 잊어서는 안 됩니다. 특정 프레임워크에 얽매이지 않은 고품질 UX 구성 요소를 사용할 수 있다는 것이 좋습니다. 다음과 같은 새로운 프레임워크를 실험할 수 있다는 것은 정말 좋은 일입니다. 고체, 탭, 모달, 자동 완성 또는 기타 구성 요소를 찾거나 함께 해킹할 필요가 없습니다.

타임 스탬프 :

더보기 CSS 트릭