과제를 위해 ThorVG Web 내부 테스트 프로그램을 구동 후 테스트하면서 코드가 어떻게 구성되어 있는지 작성하려고 합니다.
목차
1. Perf Test
Perf Test는 여러 Lottie Animation을 동일 조건에서 반복 렌더링하고 FPS, frame time, 메모리, 로딩 시간을 관찰하기 위한 프로그램입니다. 즉 renderer, Animation 개수, 크기, 에셋 목록을 관리합니다.
1.1. 전반적인 코드 구조 요약
분석 전에 코드 구조에 대해 간단히 정리하겠습니다. app/page.tsx가 테스트 조건과 렌더링 루프를 관리합니다. seed.ts는 입력받는 에셋들을 관리하고, benchmark.ts가 프레임 표본을 결과값으로 전환합니다. WebCanvas는 실제 Animation, Canvas, Picture 처리를 담당합니다.
1.2. WebCanvas 초기화 전 seed 생성
초기 화면이 URL에서 읽어오는 renderer(렌더링 백엔드), count(애니메이션 개수), size(각 셀의 크기). seed(애니메이션 이름 목록의 Base64값) 등을 읽어옴으로써 같은 query, seed를 다시 사용하여 테스트 조건을 쉽게 재현 가능합니다.
여기서 seed가 필요한 이유는 동일한 메셋 순서와 구성을 URL에서 복원하여 입력 조건을 더 효율적으로 재현하기 위해 사용되었습니다. 무작위로 선택되는 Lottie 종류가 매번 달라지면 renderer별 결과 차이가 에셋 차이 때문인지 백엔드 차이 때문인지 구별하기 어렵기 때문입니다. 아래 코드처럼 btoa()로 URL에 넣기 쉬운 Base64로 인코딩하여 query에 저장하고, 필요할 때에 atob()로 디코딩하여 해당 시드의 에셋 URL 목록을 반환합니다.
// perf-test/lib/seed.ts
export function encodeSeed(names: string[]): string {
return btoa(names.join(','));
}
export function decodeSeed(seed: string): string[] {
return atob(seed).split(',').map((s) => s.trim()).filter(Boolean);
}
1.3. WebCanvas 초기화 및 Picture 가상화
WebCanvas 초기화를 위해 npm 로드 및 렌더링 백엔드로 ThorVG를 초기화한 뒤, 적절한 thorvg.wasm 반환을 통해 화면 너비, 셀 크기, 보이는 높이에 맞게 Canvas를 생성합니다. 여기서 WebCanvas는 각 Animation/Picture들을 실제로 어떻게 그릴지 판단합니다.
애니메이션 개수가 많아도 모든 Picture을 항상 Canvas에 유지하게 되면 리소스가 소모되므로 RAF 루프에서 현재 스크롤 범위를 계산하고 각 항목의 row가 보이는 범위인지 판단합니다. 하나의 WebCanvas에서 여러 Picture을 관리하기 용이하고 화면 밖 항목까지 계속 그리는 비용을 줄일 수 있습니다.
// perf-test/app/page.tsx
const visTopGrid = -offsetGridToCanvas;
const visBotGrid = visTopGrid + canvasRect.height;
const firstRow = Math.max(
0,
Math.floor(visTopGrid / cellSize)
);
const lastRow =
Math.ceil(visBotGrid / cellSize) - 1;
// 생략
const shouldShow =
row >= firstRow && row <= lastRow;
if (
entry.picture &&
shouldShow !== entry.visible
) {
entry.visible = shouldShow;
if (shouldShow) {
tvgCanvas.add(entry.picture);
} else {
tvgCanvas.remove(entry.picture);
}
}
if (shouldShow && entry.picture) {
entry.picture.translate(
entry.posX,
entry.gridY +
offsetGridToCanvas +
entry.yOff
);
// 생략
1.4. 성능 측정
성능을 측정할 때 3초동안 안정화를 거친 뒤 10초동안 frame 간 시간 데이터를 모읍니다. 순간적인 프레임 드랍을 놓치지 않기 위해 max, p95도 함께 확인하며 같은 기기·브라우저·창 크기·seed·count·size·워밍업·측정 시간을 유지해야 합니다. 또한 이 때 JS heap 메모리는 performance.memory를 제공하는 브라우저에서만 확인 가능한 점을 유의해야 합니다.
// benchmark.ts
BENCH_WARMUP_MS = 3000;
BENCH_MEASURE_MS = 10000;
// FPS 계산 방법
Math.round(timings.length / (measureMs / 1000))
프로그램 빌드 결과는 다음과 같습니다.
2. Playground
Playground는 예제 코드를 수정하면 해당 프로그램이 코드를 실행 가능한 형태로 변환하고, WebCanvas 객체에 전달하여 결과를 바로 보여주는 WebCanvas 실험 환경입니다.
2.1. 예제 선택 및 코드 상태 관리
basic-shapes.ts와 같은 형태로 등록된 예제들은 홈 화면에서 선택할 수 있습니다. 선택한 후 예제의 코드 문자열은 code 초기값으로 저장 후, CodeEditor에서 코드가 변경되면 code 상태가 갱신됩니다. 또한 code 상태가 CanvasPreview의 prop으로 전달되어 편집기에서 변경한 코드가 미리보기의 입력으로 진행되게 됩니다.
// ShowcasePageClient.tsx
const [example, setExample] = useState(
getExampleById(id)
);
const [code, setCode] = useState(
example?.code || ''
);
const [autoRun, setAutoRun] = useState(true);
useEffect(() => {
const foundExample = getExampleById(id);
if (!foundExample) {
router.push('/');
return;
}
setExample(foundExample);
setCode(foundExample.code);
}, [id, router]);
2.2. WebCanvas 초기화
원하는 예제를 선택 후 해당 예제를 렌더링하기 위해 WebCanvas를 초기화해야 합니다. 그러기 위해 브라우저 실행 시점에 WebCanvas 모듈을 동적으로 불러오고, 선택한 renderer로 ThorVG WASM을 초기화하고 WASM 파일 위치를 제공한 후 html의 canvas와 정해진 렌더링 영역을 연결합니다.
// CanvasPreview.tsx
const { init } = await import('@thorvg/webcanvas');
const TVGInstance = await init({ renderer, locateFile: () => wasmUrl });
const canvasInstance = new TVGInstance.Canvas('#canvas', {
width: 600,
height: 600,
});
그리고 아래 코드에서 보면 편집기에서 예제의 초기화 코드를 제거하는 걸 확인할 수 있습니다. 그 이유는 CanvasPreview가 이미 WebCanvas와 Canvas를 초기화했기 때문에 예제 코드를 수정할 때 사용자 로직만 재실행하기 위해서입니다.
// code-transformer.ts
// playground/lib/code-transformer.ts
export function transformCodeForExecution(
code: string
): string {
let result = code;
// 1. import 문 제거
result = result.replace(
/^import\s+.*?from\s+['"].*?['"];?\s*$/gm,
''
);
// 위와 같은 정규식을 통해 export, TVG Canvas 생성 및 초기화, 주석, 연속된 빈 줄 정리
// 생략
}
2.3. 변환된 코드 실행
이전 결과들을 취소하고 정리한 뒤 변환된 코드는 transformCodeForExecution()으로 변환하고, requestAnimationFrame을 래핑해 animation ID를 추적하도록 설정합니다. 또한 fetch cache도 같이 준비하고 실행 함수를 만들어서 나온 결과를 WebCanvas API 호출 결과로 전달하여 Canvas에 표시합니다.
이를 통해 편집된 코드를 즉시 실행 가능하며, 초기화된 TVG와 Canvas를 재사용할 수 있씁니다. 또한 애니메이션 ID를 추적해 다음 실행 전 취소 가능하며, 외부 이미지나 Lottie Asset의 GET 결과를 캐시 가능한 장점이 있습니다.
// CanvasPreview.tsx
import { init } from '@thorvg/webcanvas';
const TVG = await init({
renderer: 'gl'
});
const canvas = new TVG.Canvas('#canvas', {
width: 600,
height: 600
});
const shape = new TVG.Shape();
shape.appendCircle(300, 300, 100, 100);
shape.fill(255, 0, 0, 255);
canvas.add(shape);
canvas.render();
프로그램 실행 결과는 다음과 같습니다.
3. 커스텀 예제
여러 Paint를 실제 편집 도구처럼 선택하고 옮기고, 목표 위치에 도형을 맞추는 예제를 구현하였습니다. code
댓글
Discussion 원문