← 목록으로
AI 시대, frontend testing 전략

AI로 코드를 작성하는 시대에 가장 중요한 건 test code입니다. AI가 만든 코드가 의도대로 동작하는지 검증할 수 있어야 하니까요. 그런데 frontend에서 testing은 여전히 어렵습니다.

frontend testing이 어려운 이유

unit test를 작성하려면 component의 상태를 mocking해야 하는데, 상태가 component 내부에 강하게 결합되어 있으면 이게 쉽지 않습니다. UI rendering과 business logic이 한 곳에 섞여 있으면 테스트할 대상을 분리하는 것 자체가 어렵습니다. 그래서 대부분 e2e test에 의존하게 됩니다.

e2e test는 실제 동작을 검증할 수 있지만, 느리고 깨지기 쉽고 실패 원인을 특정하기 어렵습니다. unit test와 integration test를 두텁게 깔고 e2e는 핵심 flow만 검증하는 게 이상적인데, frontend에서는 그게 어려웠습니다.

AI가 바꾼 것

AI가 등장하면서 test code 작성 비용이 거의 사라졌습니다. test case만 잘 설명하면 코드는 금방 나옵니다. 문제는 test 가능한 구조로 코드가 작성되어 있느냐입니다. component 내부에 상태와 로직이 얽혀 있으면 AI가 아무리 빨라도 테스트할 수 없는 건 마찬가지입니다.

결국 AI 시대에 중요한 건 test를 잘 작성하는 능력이 아니라, test 가능한 구조를 설계하는 능력입니다.

상태를 component 밖으로

시도하고 있는 방식은 effect-atom을 이용해서 모든 component 상태를 외부 atom으로 관리하는 것입니다.

import { Atom } from "@effect-atom/atom"

// component 외부에서 상태 정의
const count = Atom.make(0)
const doubled = Atom.map(count, (n) => n * 2)

component에서는 atom을 UI에 binding하고, event handler만 연결합니다.

function Counter() {
  const value = useAtomValue(doubled)
  const set = useAtomSet(count)
  return <button onClick={() => set((n) => n + 1)}>{value}</button>
}

상태와 로직이 component 밖에 있으니 React 없이도 테스트할 수 있습니다.

Effect로 business logic 분리

business logic은 Effect로 작성합니다. Effect의 핵심은 dependency injection을 통한 interface programming입니다.

class TodoRepo extends Context.Tag("TodoRepo")<TodoRepo, {
  readonly getAll: Effect.Effect<Todo[]>
  readonly create: (title: string) => Effect.Effect<Todo>
}>() {}

// business logic - TodoRepo interface에만 의존
const createTodo = (title: string) =>
  Effect.gen(function* () {
    const repo = yield* TodoRepo
    // validation, business rule 등...
    return yield* repo.create(title)
  })

test에서는 mock implementation을 주입하면 됩니다.

const TestTodoRepo = Layer.succeed(TodoRepo, {
  getAll: Effect.succeed([]),
  create: (title) => Effect.succeed({ id: "1", title, done: false }),
})

// network, DB 없이 business logic만 검증
const result = await Effect.runPromise(
  createTodo("test").pipe(Effect.provide(TestTodoRepo))
)

외부 dependency가 interface로 분리되어 있으니 mocking이 자연스럽습니다. API client, storage, analytics 등 모든 외부 의존성을 같은 방식으로 처리할 수 있습니다.

test 구조

이 구조에서 test는 세 layer로 나뉩니다.

unit test - Effect function 단위로 business logic을 검증합니다. component와 무관하게 순수 logic만 테스트합니다. mock layer를 주입하면 되니 setup이 간단합니다.

integration test - atom + Effect를 조합해서 상태 변화 flow를 검증합니다. 사용자가 action을 실행했을 때 atom이 어떻게 변하는지, side effect가 의도대로 발생하는지 확인합니다.

e2e test - 핵심 user flow만 검증합니다. unit/integration test가 두텁게 깔려 있으니 e2e는 최소한으로 유지할 수 있습니다.

정리

AI가 test code를 빠르게 만들어주는 시대에, 중요한 건 test 가능한 구조입니다. 상태를 component 밖으로 빼고, business logic을 Effect로 분리하면 frontend에서도 unit test와 integration test를 두텁게 쌓을 수 있습니다.

References