Integrate
Next.js (사용 중단)
App Router에서 안전한 클라이언트 경계로 감싼 <ComicStrip>과 SSR 플레이스홀더 헬퍼는, 이제 @toonstrip/react와 @toonstrip/cli에 직접 내장되어 있습니다.
사용 중단
@toonstrip/next는 Next.js App Router에 필요한 두 가지, 즉 'use client' 경계와
서버 전용 PNG 플레이스홀더 헬퍼를 감싸는 패키지였습니다. 이제 둘 다 실제 동작을
담당하는 패키지에 직접 포함되어 있어, 이 래퍼가 그 위에 더해 주는 것은 없습니다. 기존
설치는 그대로 동작합니다(제거된 것이 아니라 재익스포트용 shim입니다). 새로 작성하는
코드는 아래와 같이 @toonstrip/react와 @toonstrip/cli에서
가져와야 합니다.
<ComicStrip>
// app/page.tsx: Server Component입니다. 여기에는 "use client"가 필요 없습니다
import { ComicStrip } from "@toonstrip/react";
<ComicStrip document={doc} packs={["/packs/comic-chat/"]} />
@toonstrip/react의 <ComicStrip>과 동일한 props입니다.
컴포넌트 자체가 'use client' 지시어를 갖고 있는데, 이는 useRef/useEffect를 쓰는
코드 주위에 React Server Components가 요구하는 경계입니다. Server Component에서 바로
임포트할 수 있으며, 페이지 자체를 'use client'로 표시할 필요는 없습니다.
SSR 플레이스홀더
// app/page.tsx
import { ComicStrip } from "@toonstrip/react";
export default async function Page() {
// 정적 임포트가 아니라 동적 임포트 + webpackIgnore입니다. 아래 콜아웃을 참고하시기 바랍니다.
const { renderPlaceholder } = await import(/* webpackIgnore: true */ "@toonstrip/cli");
const placeholder = await renderPlaceholder(doc, { width: 640 });
return <ComicStrip document={doc} packs={["/packs/comic-chat/"]} placeholder={placeholder} />;
}
renderPlaceholder는 toonstrip render가 사용하는 것과 같은
@toonstrip/cli의 헤드리스 캔버스 렌더링 경로를 재사용해, 서버에서 PNG를 생성합니다.
크기는 넘겨준 width 값에 맞춰 CSS 픽셀 단위로 정해집니다. <ComicStrip>은 이 PNG를
<comic-strip> 내부의 라이트 DOM <img slot="placeholder">로 렌더링하며, 이는 원본
서버 HTML에 클라이언트 JS 없이도 그대로 담겨 있으므로 자바스크립트가 꺼져 있어도,
그리고 “소스 보기”에서도 나타납니다. 클라이언트 JS가 하이드레이션되고 라이브
캔버스가 준비되면 @toonstrip/element가 플레이스홀더를 숨기고 라이브 스트립을
보여줍니다. 이 전환을 직접 제어하는 코드는 없습니다.
renderPlaceholder에 넘기는 width는 실제로 <ComicStrip>이 배치되는 CSS 너비와
일치시켜야 합니다. 값이 어긋나면 플레이스홀더가 라이브 스트립과 다른 크기로 표시되고,
하이드레이션 시점에 레이아웃 시프트로 나타납니다.
renderPlaceholder는 @toonstrip/cli를 끌어오는데, 이 패키지는 네이티브 Node 애드온인
@napi-rs/canvas를 로드하므로 클라이언트 번들에 절대 들어가서는 안 됩니다. Server
Component나 그 밖의 서버 전용 코드에서만 호출하시기 바랍니다.
Warning
@toonstrip/cli의 네이티브 의존성 때문에 앱의 next.config에 두 가지가 필요합니다.
@napi-rs/canvas와 @toonstrip/cli를 serverExternalPackages에 등록하고,
@toonstrip/cli를 앱의 직접 의존성으로도 추가해야 합니다(단순한 전이 의존성이 아니라).
이 두 가지를 모두 해도, @toonstrip/cli가 이 모노레포의 예제처럼 워크스페이스로
심볼릭 링크된 로컬 패키지인 경우 renderPlaceholder를 정적으로 임포트하면
webpack이 네이티브 바이너리까지 번들링하려 시도합니다. 위에서 webpackIgnore
처리된 동적 import()를 쓰는 이유가 여기에 있습니다. 실제로 동작하는 설정은
examples/next-app의 next.config.mjs와 app/page.tsx를 참고하시기 바랍니다.
예제
레포의 examples/next-app은 App Router 앱으로, Server Component 페이지가 플레이스홀더와
함께 스트립을 렌더링합니다. scripts/copy-pack-assets.mjs는 클라이언트 쪽 캐스트
페치를 위해 @toonstrip/pack-comic-chat의 파일들을 public/packs/comic-chat/로
복사하며(examples/react-vite와 examples/vue-vite가 쓰는 것과 같은 스크립트입니다),
renderPlaceholder는 서버 사이드 렌더링을 위해 node_modules에서 팩을 직접 읽습니다.
로컬에서 실행하려면 다음과 같이 합니다.
pnpm --filter next-app-example build && pnpm --filter next-app-example start
현재 제약 사항
- Next.js 15(App Router)와 React 18을 기준으로 테스트되었습니다. Pages Router는 다루지 않습니다.
- 아직 Nuxt에 해당하는 래퍼는 없습니다. 관련 내용은 Vue 레퍼런스의 설명을 참고하시기 바랍니다.
Tip
@toonstrip/element 모듈은 이제 서버에서 임포트해도 오류가 발생하지 않습니다. 이 가드
처리(개발 계획 §13.4)가 애초에 <ComicStrip>의 클라이언트 경계를 Server Component에서
안전하게 임포트할 수 있게 해 주는 부분입니다.