사람함께에이전트▾
사람 — 직접 정하고 책임지는 비즈니스 규칙과 흐름. 직접 읽어보세요.
함께 — 개념은 알아두고, 세부 규칙은 에이전트가 따릅니다.
에이전트 — 에이전트가 따르는 규칙과 레퍼런스. 필요할 때 찾아보세요.
지연 로딩
지연 로딩은 무거운 컴포넌트를 첫 다운로드에서 빼 두는 방식입니다. 그 코드는 사용자가 실제로 그 컴포넌트를 쓸 때 내려받으며,
akanjs/webkit의 lazy()로 구현합니다.이 페이지에서 쓰는 말
용어설명
청크(chunk)
번들러가 따로 떼어 낸 JavaScript 파일로, 브라우저는 필요해질 때만 이 파일을 받습니다.
Suspense 경계
안쪽 내용이 아직 로딩 중일 때 그 자리에 자리 표시(placeholder)를 대신 보여 주는 React 경계입니다.
shell
서버가 가장 먼저 보내는 HTML로, 경계 안쪽 내용은 그 뒤에 스트리밍으로 따라올 수 있습니다.
무엇을 나눌까
컴포넌트
lazy()
import
떼어 낼 것
지도 · 차트 · 에디터 · 3D 뷰어 · 지갑 위젯
✓
코드가 무겁고, 브라우저에서만 도는 경우가 많습니다.
가끔만 여는 큰 관리자 패널
✓
대부분의 방문에서는 열리지 않으니, 그 비용도 치르지 않습니다.
그냥 import할 것
작은 버튼
✓
따로 받을 만큼 크지 않습니다.
첫 화면의 핵심 내용
✓
사용자가 바로 봐야 하므로, 미루면 기다리게 할 뿐입니다.
✓이 방식으로 불러옵니다이 방식이 아닙니다
lazy() 옵션
lazy(loader, option?)는 import(…)를 돌려주는 함수를 받아, 평소처럼 렌더링하는 컴포넌트를 돌려줍니다.ssrboolean기본값 true
false면 서버 렌더링을 건너뛰어 서버는 loading만 보내고, 청크는 마운트 뒤에 받습니다.suspenseboolean기본값 false
컴포넌트를 전용 Suspense 경계로 감싸, 청크를 기다리는 동안 이 자리만 기다리게 합니다.
loading() => ReactNode
ssr: false나 suspense: true일 때만 보이는 자리 표시입니다.외부 라이브러리
어떤 라이브러리는 크거나, 불러오는 순간
window 같은 브라우저 전용 API를 건드립니다. 이런 라이브러리는 ui/<Folder>/의 파일 두 개를 거쳐 불러오고, 브라우저가 필요하면 ssr: false로 서버 렌더링을 끕니다.index_.tsx는"use client"로 시작하고lazy()컴포넌트를 내보냅니다.index.tsx에는"use client"가 없습니다../index_에서 가져오며, 앱의 다른 파일은 이 파일을 import합니다.
먼저 라이브러리를 불러오는 클라이언트 파일입니다.
apps/koyo/ui/ArticleMap/index_.tsx
다음은 페이지가 import하는, 서버에서도 안전한 컴포넌트입니다.
apps/koyo/ui/ArticleMap/index.tsx
- 파일이 두 개인 이유.
"use client"파일에서 내보낸 namespace는 서버에 스텁(stub) 하나로 도착해서,X.Member가undefined가 됩니다. namespace와 감싸는 컴포넌트는index.tsx에 만들고, 두 파일을 합치면 RSC가 깨집니다. lazy()는 모듈의default를 렌더링합니다. 그래서lazy()대상에는export default를 씁니다. named export라면 loader가 그 컴포넌트를 돌려주게 합니다:import("./X").then((m) => m.X).- 설정이 필요하면 내 파일로 감쌉니다. 플러그인 등록 같은 준비가 필요하면
export default를 가진 옆 파일을 만들고 그 파일을 lazy로 불러옵니다.libs/util/ui/Chart가 이렇게 합니다. - 페이지는 패키지를 직접 import하지 않습니다. 페이지와 모듈 파일은 서드파티 패키지를 import할 수 없으므로, 패키지는 이
ui/폴더로만 들어옵니다.
큰 컴포넌트
직접 만든 컴포넌트도 같은 방식으로 나눕니다. 클릭한 뒤에야 열리는 무거운 에디터나 대시보드에서 효과가 가장 큽니다.
페이지가 그려진 뒤에 마운트되는 것에는
suspense: true로 전용 경계를 둡니다.apps/koyo/ui/ArticleEditor/index_.tsx
필요할 때만 렌더링합니다. 청크는
open이 처음 true가 될 때 받습니다.apps/koyo/ui/ArticleEditor/index.tsx
suspense: true를 빼면 페이지 전체가 깜빡입니다. 기다림이 가장 가까운 경계(보통 route)까지 올라가서, 처음 열 때 페이지 전체가 로딩 화면으로 다시 그려집니다.- 페이지 본문에는
suspense: true를 쓰지 않습니다. 스트리밍 SSR에서는 경계 안쪽이 shell에서 빠지고 나중에 도착합니다. SEO 스냅샷, 프리렌더링, hydration 전 E2E는 shell만 읽으므로 그 내용을 놓칩니다. akanjs/ui도 이렇게 합니다.Model.*의 모달과 래퍼는 모두suspense: true를 단lazy()export입니다.


loading만으로는 아무것도 보이지 않습니다. suspense: true나 ssr: false가 없으면 컴포넌트에 자기 경계가 없어서, 넘긴 자리 표시가 한 번도 그려지지 않습니다.서버 어댑터
서버에도 같은 원리가 적용되며, 여기서는 비용이 번들 크기가 아니라 메모리입니다. 모듈 최상위에서 import한 무거운 SDK는 앱이 설정하지 않아도 모든 replica와 batch worker에 상주합니다.
import 하나가 비싼 이유
- barrel이 전부 불러옵니다. 생성된
srvkit/index.ts는 모든 adapter를 re-export하므로, helper 하나만 import해도 그 폴더가 import하는 SDK가 모두 로드됩니다. - 생성만 막아서는 부족합니다.
options.discord ? new DiscordApi(...) : null은 객체만 건너뛸 뿐, 파일 맨 위의 import는 이미 실행된 뒤입니다.
무엇을 미룰까
이 워크스페이스에서 SDK를 바로 import한 채로 측정하면, 아래 표의 처음 네 개만으로 요청이 하나도 오기 전에 약 61 MiB가 상주합니다.
패키지
import()
import
미룰 것: 무겁고, 앱이 설정하지 않을 수도 있음
discord.js
✓
Discord 메시지 전송용이며, 약 23 MiB입니다.
puppeteer
✓
PDF 생성에 쓰는 헤드리스 브라우저이며, 약 19 MiB입니다.
nodemailer
✓
메일 발송용이며, 약 16 MiB입니다.
firebase-admin
✓
푸시 알림용이며, 약 2 MiB입니다.
이미지 인코더
✓
무겁고, 이미지를 다루는 앱에만 필요합니다.
그대로 둘 것: 매 요청에 쓰임
jwt · aes
✓
미뤄도 로딩이 첫 요청으로 옮겨 갈 뿐입니다.
✓이렇게 import합니다이렇게 하지 않습니다
미루는 방법
- 타입은
import type으로 남깁니다. 빌드에서 지워지므로 시그니처는 그대로입니다. - 값 import는 모듈 수준 promise에 담고
??=로 한 번만 만듭니다. - SDK는 async 메서드 안에서 꺼냅니다. 그러면 첫 호출 때 로드됩니다.
adapt() 클래스에서는 이렇게 씁니다.apps/koyo/srvkit/discordApi.ts
- 첫 호출 전에는 아무것도 불러오지 않습니다. 메시지를 한 번도 보내지 않는 프로세스는
discord.js를 끝내 불러오지 않습니다. - 동시에 불러도 로딩은 한 번입니다.
??=가 첫 promise를 붙잡아 두므로,send()를 두 번 동시에 불러도 import도 로그인도 한 번만 합니다.
측정하기
바꾸기 전과 후에 프로세스가 실제로 치르는 비용은 아래 환경 변수로 확인합니다.
AKAN_MEMORY_LOG"1"
서버 프로세스마다 상주 메모리(RSS)를 주기적으로 로그에 남깁니다.
AKAN_MEMORY_LOG_INTERVAL_MSnumber기본값 60000
보고 주기이며, 단위는 밀리초입니다.
서버 렌더링 또는 클라이언트 전용
세 가지 설정은 서버가 컴포넌트를 그리는지, 자리 표시가 보이는지가 다릅니다. 컴포넌트에 필요한 것을 보고 고릅니다.
설정
서버 렌더링
loading 표시
서버에서 그리는 설정
lazy(loader)
✓
기본값이며, 서버에서 그릴 수 있는 컴포넌트에 씁니다.
{ suspense: true }
✓
✓
클릭한 뒤에 마운트되는 모달 본문, 에디터, 드롭다운에 씁니다.
브라우저에서만 그리는 설정
{ ssr: false }
✓
라이브러리가
window, document, canvas, WebGL, 브라우저 저장소를 필요로 할 때 씁니다.✓예아니요
- 기본값에서는 가장 가까운 경계가 기다립니다. 자기 경계가 없어서 바깥 경계가 청크를 기다리고(더 가까운 경계가 없으면 route),
loading은 보이지 않습니다. suspense: true는 이 자리만 기다립니다. 서버에서는 내용이 shell 뒤에 스트리밍으로 도착합니다.ssr: false면 서버는loading만 보냅니다. 자리 표시는 마운트 전까지, 그리고 청크를 받는 동안 계속 보입니다. 이 설정에는 항상 전용 Suspense가 있어서suspense: true를 더해도 달라지는 것이 없습니다.- 빈자리가 어색하면 자리 표시를 둡니다. 실제 컴포넌트와 같은 크기(
h-64 w-full)로 두면 컴포넌트가 도착할 때 화면이 튀지 않습니다.
팁
- 사용자 의도 단위로 나눕니다. 에디터, 지도, 차트, 모달, 뷰어가 좋은 단위입니다.
- 사용자가 처음 봐야 하는 것은 lazy로 불러오지 않습니다. 보이기 전까지 기다림만 늘어납니다.
- 여러 곳에서 늘 쓰는 컴포넌트는 얻는 것이 없습니다. 많은 페이지가 같은 컴포넌트를 바로 그린다면, lazy는 지연만 더할 수 있습니다.
이어서 볼 문서