1. 프론트엔드에서 테스팅은 필수일까
프론트엔드 진영에서 테스팅은 필수로 여겨지지 않는 경우가 제법 많습니다. 각기 다른 이유가 있겠지만 제 경험상 저는 ‘바빠서'가 주된 이유였던 것 같습니다. unit testing, integration testing, e2e testing 모두 100%의 커버리지로 테스팅을 진행해서 나쁜 점이야 없겠다만, 우리의 외부환경은 기다려 줄 수 없습니다. 특히 스타트업에서 프론트엔드는 유난히 변경이 크게 일어나기 때문에 바로 버려지는 테스트코드는 시간낭비처럼 느껴집니다.
경험적으로 불필요하게 느껴지지만 종종 사소한 실수로 핫픽스를 올리는 경우가 일어난다면 테스팅에 대한 고민은 꼭 해볼법 합니다. 결론적으로 테스트를 하던 안하던 결국 우리 모두가 가장 원하는 것은 ‘높은 품질의 제품이 빠른 시간 안에 개발 되는 것’입니다. 테스트 코드가 만약에 우리의 목표지점에 더 정확하고 빠르게 갈 수 있도록 도와준다면 필요하다고 판단됩니다.
2. 프론트엔드 테스팅 경험과 삽질
멋진 도구들(cypress나 storybook)을 사용하여 호기롭게 ui 컴포넌트 테스팅을 도입했지만 하루가 다르게 변경되는 요구사항에 포기를 한 경험은 필시 저 뿐만은 아닐겁니다. 특히나 cypress나 storybook은 단 한번의 삽질 없이 도입하여 유려하게 사용이 가능 했던 적은 아마도 잘 없지 않을까 싶습니다. 심지어 jest로 typescript와 리액트 설정은 또 얼마나 복잡하고 귀찮은가요? 삽질의 시간이 고통스럽고 길어질수록 ‘개발은 나랑 잘 안맞는것 같다’ 같은 생각도 더러 드는 경우도 있습니다.
특히나 e2e 테스팅의 경우 아직 de facto라고 불릴만한 도구는 개인적으로 아직 없는 것 같습니다. 아래 npm trendsd에서 cypress, playwright, puppeteer의 트렌드를 비교해보았습니다. 그래도 고르라면 저는 가파르게 상승하고 있는 playwright을 고르지 않을까요.
실제로 프론트엔드 커뮤니티에서 cypress의 대체제로 playwright을 많이 꼽고 있습니다.

3. 가성비 좋은 테스팅
만약에 서비스가 가파르게 성장하고 제품의 변경이 시간단위로 일어난다면 테스팅은 정말로 사치와 같아집니다. 그럼에도 불구하고 프론트엔드에서 ui를 보여주는 ‘계산’ 로직은 분명 유닛 테스팅을 도입할 만 합니다. 개인적으로 가성비가 좋다고 생각되는 이유는 아래와 같습니다:
- 계산 로직을 테스팅 함으로서 프론트엔드에서 순수 ui와 계산 로직을 분리하도록 유도하여 코드품질을 올려줍니다. 그리고 개발이 재밌어집니다.
- 정말 많은 프론트엔드 버그는 사소한 계산로직 실수에서 나옵니다. 이를 방지할 수 있습니다.
- console.log 찍어가며 개발 하는 것보다 빠르게 개발 가능합니다.
- 개발이 100배 더 재밌어집니다.
4. 테스팅 레츠고
평소에 실무에서 있을만한 케이스로 바로 증명하겠습니다. 시작하기에 앞서 아래와 같은 조건, 환경과 방법으로 진행하겠습니다:
요구사항: 카운트를 ‘분:초’ 단위로 표시 (ex: 9 -> 00:09, 62 -> 01:02)
개발환경: nextjs, react, vitest
아래 순서대로 진행(TDD):
- Try and run the test
- Write the minimal amount of code for the test to run and check the failing test output
- Write enough code to make it pass
- Refactor
4.1 Vitest 설치
jest와 mocha와 같이 훌륭한 도구도 있지만 vitest로 진행했습니다. 최신 도구인만큼 nextjs와 같은 모던한 환경에서 configuration 최적화가 잘 되어있을 거라고 생각했습니다. 실제로 configuration 할 게 거의 없습니다.
# 설치 스크립트 (저는 신세대라 bun 사용하겠습니다)
$ bun add -D vitest
# 설치 후 config 파일 생성
$ touch vitest.config.ts
// vitest.config.ts
// https://github.com/vercel/next.js/blob/canary/examples/with-vitest/vitest.config.ts
// 위 링크에서 복붙한 config입니다. 굳이 alias와 plugins 옵션은 필수가 아닙니다.
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
import path from "path";
export default defineConfig({
plugins: [react()],
test: {
environment: "jsdom",
},
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
});
4.2 테스팅 시작
src폴더에 __tests__ 폴더를 만들어 이 안에 테스트 파일을 보관하겠습니다. vitest가 테스팅 파일을 알아서 찾아줍니다. 따로 설정할 필요가 없습니다.
4.2.1 Try and run the test
간단한 예제입니다. 계산을 진행하는 코드 전에 테스트 코드를 먼저 작성합니다.
// src/__tests__/time-formatter-util.test.ts
import { describe, expect, test } from "vitest";
describe("Time formatter utils", () => {
test("test counter format to minute and seconds", () => {
const expected = "00:09";
const count = 9;
expect(TimeFormatterUtils.secondsToMMSS(count)).toBe(expected);
});
});
# vitest로 HMR처럼 테스팅을 돌립시다
$ vitest
바로 test가 실패했습니다. 에러메시지를 보면 친절하게 모든걸 알 수 있습니다.

4.2.2 Write the minimal amount of code for the test to run and check the failing test output
최소의 코드로 테스트가 pass 할 수 있도록 코드를 작성하겠습니다.
// src/utils/time-formatter.util.ts
export class TimeFormatterUtils {
public static secondsToMMSS(seconds: number) {
return "00:09";
}
}

4.2.3 Write enough code to make it pass
이제 요구받은 사항대로 코드를 작성해보겠습니다. 시간계산은 date-fns를 사용하였으며, util 함수의 경우 편한 관리를 위해 class로 표현했습니다. util함수들을 일반 함수로 두다보면 파편화되어 관리가 난처해지는 경우도 있습니다. class를 통하여 한 곳에 몰아넣고 접근제어자를 통해 관리하면 관련된 util함수가 늘어나도 관련된 util class에서 응집도 있게 코드를 유지할 수 있습니다.
// src/utils/time-formatter.util.ts
import { intervalToDuration } from "date-fns/fp";
export class TimeFormatterUtils {
public static secondsToMMSS(seconds: number) {
const duration = intervalToDuration({ start: 0, end: seconds * 1000 });
return (
this.formatTimeDigit(`${duration.minutes}`) +
":" +
this.formatTimeDigit(`${duration.seconds}`)
);
}
private static formatTimeDigit(time: string) {
if (time.length < 2) {
return `0${time}`;
}
return time;
}
}

4.2.4 Refactor
코드 자체가 간단했던만큼 크게 리팩터할 부분은 없지만 formatTimeDigit을 간소화 시켰습니다. 테스트코드를 통해 코드가 망가지는 걱정 없이 마음편히 리팩토링을 할 수 있습니다. 리스크 없이 걱정없이 더 나은 코드를 위해 깊게 고민해볼 수 있습니다. 이로써 개발 자체가 100배 더 즐거워 질 수가 있습니다.
// src/__tests__/time-formatter-util.test.ts
export class TimeFormatterUtils {
public static secondsToMMSS(seconds: number) {
const duration = intervalToDuration({ start: 0, end: seconds * 1000 });
return (
this.formatTimeDigit(`${duration.minutes}`) +
":" +
this.formatTimeDigit(`${duration.seconds}`)
);
}
private static formatTimeDigit(time: string) {
return time.length < 2 ? `0${time}` : time;
}
}

5. Caveats
해당 예제를 통해 간단한 유닛 테스팅으로 개발이 얼마나 더 재밌어질 수 있는지 알아봤습니다. 다만 블로그에 올리기 위한 간단한 예제이므로 몇가지 놓치는 점들이 있습니다:
- 테스트 도입에 대해 초점을 맞추기 위해 좋은 테스트 케이스란 무엇인가에 대해서 고민이 제외되었습니다.
- 이에 따라 예제가 요구하는 구현사항에 헛점들이 많습니다. 가령 3600초 (1시간)일 경우, 6000초일경우, 또는 초가 음수로 주어질 경우에 대한 케이스를 고려하지 않았습니다.
6. 정리
- 프론트엔드에서 의외로 계산 로직이 많습니다. 가급적 UI와 계산로직을 분리시켜 관리하고 유닛 테스팅을 통해 똑똑하고 빠르게 개발할 수 있습니다. console.log 찍어가며 개발하는 것보다 더 나은 방법입니다.
- Vitest설치는 딱히 할 게 없을정도로 간단합니다. 모던 프레임워크를 사용한다면 어디에나 삽질없이 붙일 수 있습니다.
- 테스트코드를 먼저 작성하고 그에 맞춰서 로직을 작성하는 tdd 방식을 사용한다면 더 재밌게 개발이 가능합니다.
- 이 모든 과정은 더 좋은 품질의 코드를 빠르게 개발을 하기 위해 하는 것입니다. 이 모든 과정은 이루고자 하는 목적을 명확하게 하고 trade off를 고려하며 진행해야 합니다.
references: