AI로 코드를 작성하는 시대에 가장 중요한 건 test code입니다. AI가 만든 코드가 의도대로 동작하는지 검증할 수 있어야 하니까요. 그런데 frontend에서 testing은 여전히 어렵습니다.
unit test를 작성하려면 component의 상태를 mocking해야 하는데, 상태가 component 내부에 강하게 결합되어 있으면 이게 쉽지 않습니다. UI rendering과 business logic이 한 곳에 섞여 있으면 테스트할 대상을 분리하는 것 자체가 어렵습니다. 그래서 대부분 e2e test에 의존하게 됩니다.
e2e test는 실제 동작을 검증할 수 있지만, 느리고 깨지기 쉽고 실패 원인을 특정하기 어렵습니다. unit test와 integration test를 두텁게 깔고 e2e는 핵심 flow만 검증하는 게 이상적인데, frontend에서는 그게 어려웠습니다.
AI가 등장하면서 test code 작성 비용이 거의 사라졌습니다. test case만 잘 설명하면 코드는 금방 나옵니다. 문제는 test 가능한 구조로 코드가 작성되어 있느냐입니다. component 내부에 상태와 로직이 얽혀 있으면 AI가 아무리 빨라도 테스트할 수 없는 건 마찬가지입니다.
결국 AI 시대에 중요한 건 test를 잘 작성하는 능력이 아니라, test 가능한 구조를 설계하는 능력입니다.
시도하고 있는 방식은 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 없이도 테스트할 수 있습니다.
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는 세 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를 두텁게 쌓을 수 있습니다.