ghlee.dev

my profile picture

프론트엔드 애니메이션 회귀 테스트?

2025. 3. 26.

지인 개발자의 질문이 사고의 출발점이었다. “Canvas로 만든 애니메이션을 테스트하고 싶은데, 방법이 있을까요?”

처음에는 촬영된 두 이미지의 픽셀 비교를 이용하는 회귀 테스트가 생각났고, 이를 활용해서 애니메이션의 특정 순간을 앞뒤 프레임을 비교하면 어떨까 생각했다.

지인 개발자와의 대화 내용

“흐으으음” 반응에서 알 수 있듯, 현실적인 방법은 아니지도 모르겠다 🤭

문득 프론트엔드에서 사용하는 애니메이션을 영상으로 촬영한 후, 두 영상을 비교하는 방법도 있을 수 있겠다는 생각이 들었다. 또한, 게임 개발 분야에서는 규모가 큰 게임사들이 유사한 방식으로 게임 플레이 영상을 활용해 자동화 테스트를 진행한다고 들은 기억이 났다. 완전히 비현실적인 방법은 아니라고 생각했다.

애니메이션에는 CSS와 Canvas 등 브라우저에서 시각적으로 움직이는 모든 것을 포함한다.

프론트엔드 환경에서 시도해볼 만한 방법을 찾아보던 중, SSIM이라는 값을 알게 되었고, 이를 이용하면 가능할지도 모르겠다는 생각에 바로 실행에 옮겼다.

결과부터 말하자면, 방법은 생각보다 간단했고, 어느정도 신뢰할 만한 회귀 테스트도 가능했다.

SSIM

우선 SSIM에 대해 간단히 설명할 필요가 있을 것 같다. SSIM은 “Structural Similarity Index”의 줄임말로, 번역하면 “구조적 유사 지수”이다. 동영상 전처리 도구로 유명한 FFmpeg에서는 이를 “Structural Similarity Metric”이라고 부르기도 한다.

SSIM은 이미지의 시각적 회귀 테스트에서 사용되는 방식처럼 단순한 픽셀 차이를 비교하지 않는다. 밝기, 대비, 구조 정보를 동시에 고려하여 사람의 실제 시각적 인지와 유사하게 비교한다. 픽셀이 한두 개 다르더라도 앞서 언급한 세 가지 정보를 종합적으로 고려하여 두 영상의 유사성을 판단한다.

실제로 애니메이션을 녹화하는 환경은 시시각각 달라진다. 우리는 브라우저에서 애니메이션을 촬영해야 하는데, 알다시피 브라우저 환경은 네트워크와 렌더링 속도에 따라 애니메이션의 시작과 끝이 매번 달라질 수밖에 없다.

이 때문에 단순한 픽셀 차이 비교로는 회귀 테스트가 불가능하며 (높은 확률로 회귀 테스트는 실패할 것이다), 다양한 정보를 종합적으로 고려하는 방법이 필요하다.

필자가 지인 개발자에게 처음 얘기했던 방법도 불안정한 녹화 환경을 보완하기 위한 아이디어였다.

SSIM은 프론트엔드 환경에서 애니메이션 회귀 테스트에 적합할 수 있다고 판단했다. SSIM에 대한 개념 설명은 이 정도로 마무리하고, 자세한 계산 방법은 후순위로 미뤄두겠다. 우선 이를 이용해 회귀 테스트 환경을 구성해보자.

이외에도 MSE, PSNR과 같은 방법이 있다. MSE는 앞서 언급한 단순한 픽셀 비교와 유사하며, PSNR은 MSE 결과를 기반으로 데시벨(dB) 단위로 변환하는 방법이다. 그러나 두 방법 모두 사람의 시각적 인지와 큰 차이가 발생할 수 있어 애니메이션 회귀 테스트에는 부적합하다.

SSIM을 이용한 회귀 테스트

회귀 테스트를 진행하려면 우선 테스트할 두 영상이 필요하다. 이는 헤드리스 브라우저 기반의 오픈소스 라이브러리인 Playwright를 이용하면 간편하게 녹화할 수 있다.

TypeScript
import { chromium } from "playwright";
 
const RECORD_PATH = './records/';
const ANIMATION_DURATION = 5000;
const URL = 'https://test.com'
 
const browser = await chromium.launch();
const context = await browser.newContext({
  recordVideo: { dir: RECORD_PATH, size: { width: 1280, height: 720 } },
});
 
const page = await context.newPage();
await page.goto(URL);
await page.waitForTimeout(ANIMATION_DURATION);
await browser.close();

테스트할 페이지 URL을 정하고, 브라우저의 새 컨텍스트(Context)에서 녹화한 영상을 저장할 경로와 영상의 크기를 지정해주면 된다.

여기서 고민할 부분 중 하나는 애니메이션이 실행될 페이지를 어떻게 구성할 것인가인데, 개인적으로 스토리북의 각 스토리 iframe 페이지를 사용하는 것을 추천한다. 스토리북은 불필요한 요소 없이 하얀색 배경에서 애니메이션만 녹화할 수 있는 적합한 도구라고 생각한다.

스토리 iframe에 관한 내용은 필자의 다른 글에서도 다룬 바 있다.

다음으로는 애니메이션 시간을 설정해야 하는데, 자동으로 애니메이션의 시작과 끝을 알 수 있는 방법에 대해 고민할 필요가 있다. CSS 애니메이션이라면 animationend 이벤트를 감지하여 녹화를 종료할 수 있을 것이다.

하지만 Canvas 등으로 그려진 애니메이션을 감지하는 방법은 조금 까다롭다. 왜냐하면 애니메이션 종료 시 발생하는 이벤트가 자체적으로 없기 때문이다. 대신 애니메이션을 구현한 로직에 애니메이션 종료 시 postMessage로 이벤트를 보내는 방법을 사용하면, Playwright에서 이를 감지하여 활용할 수 있을 것이다. 예를 들면 아래와 같은 방법이다.

TypeScript
// 애니메이션 종료 시에 추가할 코드
window.postMessage({ type: "animationEnd" }, "*");
TypeScript
// Playwright에서 사용할 코드
await page.evaluate(() => {
    return new Promise<void>((resolve) => {
        window.addEventListener("message", (event) => {
            if (event.data.type === "animationEnd") {
                resolve();
            }
        });
    });
});

애니메이션의 시작과 끝을 정확히 녹화할수록 SSIM 값의 신뢰도가 높아진다. 종료된 화면이 불필요하게 오래 녹화될수록 동일한 프레임 이미지가 많아진다는 의미이고, 이는 SSIM 값에 영향을 미친다.

비교할 두 영상을 녹화했다면 남은 것은 FFmpeg를 이용해서 SSIM 비교 기능을 사용하는 것이다. FFmpeg는 오디오와 비디오 전처리에 사용하는 매우 유명한 도구이다. 오픈소스 프로젝트이며 C언어로 작성되어 있다.

FFmpeg에서 SSIM을 사용하는 방법은 문서를 통해 확인할 수 있으며, 아래는 코드로 구현한 예제이다.

TypeScript
const video1 = args[0];
const video2 = args[1];
 
const process = new Deno.Command("ffmpeg", {
  args: ["-i", video1, "-i", video2, "-lavfi", "ssim", "-f", "null", "-"],
  stdout: "piped",
  stderr: "piped",
});
 
const { stderr } = await process.output();
const outputText = new TextDecoder().decode(stderr);
 
const allMatch = outputText.match(/All:(\d+\.\d+)\s+\((\d+\.\d+)\)/);
const ssimScore = parseFloat(allMatch[1]);
console.log(ssimScore);

필자는 서버 환경으로 Deno를 사용했지만, Node.js도 전혀 문제 없다.

FFmpeg에 사용한 인자(args)를 설명하면 아래와 같다.

  • -i
    • 입력 파일을 지정하는 옵션이다.
  • -lavfi ssim
    • 비디오 필터를 적용하는 옵션으로, 여기서는 ssim 을 적용했다.
  • -f null
    • 출력 형식을 지정하는 옵션이며, 여기서는 실제 출력 파일을 생성하지 않도록 했다.

FFmpeg를 이용해서 구한 SSIM 값을 보기 좋게 정리해서 아래와 같이 출력할 수 있다.

image.png

일반적으로 SSIM 값이 0.95 이상이면 두 영상은 매우 유사하거나 동일하다고 볼 수 있다. 테스트 통과 여부를 결정할 기준 값은 특별히 정해진 것이 아니므로, 개발 환경에 맞춰 적절한 값을 찾아 설정하는 것이 좋다고 생각한다.

마치며

SSIM을 이용해 프론트엔드 애니메이션의 회귀 테스트를 시도해보았다. 유의미한 결과를 얻었지만, 이를 실무에서 적용하는 데는 여전히 미지수이다. 필자는 명확하게 다른 두 애니메이션을 이용해 테스트를 진행했기 때문에 SSIM 값에 유의미한 변화가 있었지만, 사소한 애니메이션의 변화는 감지하기 어려울 것으로 생각한다. 다만, 개발 과정에서 원래 들어갔어야 할 애니메이션이 아닌 엉뚱한 것이 적용되는 사이드 이펙트를 방지하는 데에는 유효한 방법이라 생각한다.