배너

세금 환급 자동화 : AI-driven UI 테스트 자동화 일지

정수호
2025년 12월 24일

안녕하세요, 토스인컴 QA Manager 정수호입니다.

토스인컴의 세금환급 서비스는 복잡합니다. 연말정산, 현금영수증, 세금비서, 숨은환급찾기 네 가지 서비스가 각각 다른 UX와 인증 방식을 사용하고, 공제 항목만 해도 의료비·인적공제·전월세·주담대·중소기업 감면 등 수십 가지가 넘어요. 여기에 실험 그룹, 약관 종류, 홈택스 스크래핑 서버 상태까지 더해지면 테스트해야 할 플로우는 금세 수십 가지 조합으로 늘어나죠.

2025년 7월, 저는 이 모든 것을 혼자 커버해야 하는 상황에 놓였습니다.

자동화가 필요하다는 건 모두가 알고 있었지만, 현실적인 문제가 남았어요. E2E 테스트 하나를 만드는 데 4~8시간이 걸리고, 40개를 만들려면 160~300시간이 필요했습니다. UI나 정책이 바뀔 때마다 테스트를 다시 손봐야 했고, 혼자 만들고 운영하고 회고까지 해야 했죠.

그래서 저는 조금 다른 질문을 던졌습니다.

“QA가 코드를 직접 치지 않아도 된다면 어떨까?” “AI를 진짜 팀원처럼 활용해서, 99%를 맡기면 어떨까?”

이 글은 그 질문을 2025년 7월부터 11월까지 진짜 서비스 환경에서 5개월 동안 실험한 이야기입니다. 결론부터 말하면, AI 3명 + 사람 1명이 함께 4~5명 규모의 자동화 팀이 낼 만큼의 결과를 만들었어요.

왜 AI였을까?

세금 환급 서비스 자동화는 일반적인 웹 서비스보다 난이도가 훨씬 높았습니다. 단순히 단계가 많기 때문이 아니라, 각 단계가 서로 다른 시스템·정책·외부 연동에 의존하며 동시에 움직이기 때문이에요.

1. 플로우 자체가 복잡했습니다

환급 플로우는 평균 15~20단계를 넘어갑니다. 그 자체로도 길지만, 더 큰 문제는 각 단계가 서로 다른 시스템 특성을 가진다는 점이었어요.

이 모든 과정이 한 번에 이어지는 장거리 릴레이처럼 작동합니다. 단 한 단계라도 타이밍이 어긋나거나 외부 시스템 상태가 달라지면 전체 플로우가 실패할 수 있어요. 게다가 UI는 React 기반입니다. 사용자에게는 자연스러운 애니메이션이나 오버레이가 E2E 테스트 입장에서는 클릭 미스, 전환 실패, 대기 타임아웃으로 이어지곤 해요.

그래서 자동화 관점에서는 정상 케이스를 한 번 구현하는 것보다, 매일 안정적으로 반복 실행하게 만드는 것’이 훨씬 어려운 일이었습니다.

2. UI와 정책이 ‘자주 그리고 크게’ 바뀌었습니다

외부에서 보면 가벼운 UI 수정처럼 느껴질 수 있지만, 자동화에서는 매우 큰 변경입니다. 예를 들어:

즉, 사람에게는 사소해 보이는 UI 변화가, 자동화에게는 시나리오 전체를 다시 구성해야 하는 큰 변경으로 다가오죠. 실제로 7월부터 11월까지 내부 변경 내역을 보면 난이도가 체감됩니다.

이런 변화들은 개발자에게는 금방 고칠 수 있는 UI 수정일지 몰라도, 자동화 테스트는 셀렉터, 대기 전략, 분기 흐름을 모두 다시 손봐야 하는 수준이었습니다. 그래서 하드코딩 된 테스트는 매번 흔들릴 수밖에 없었어요.

3. 불안정한 환경

실험 그룹에 따라 화면이 달라지고, 스크래핑 탭이 열렸다 닫혔다 하며 Target closed가 발생하고, 도메인도 환경마다 달랐습니다. 이런 상황에서 35개의 시나리오를 모두 자동화로 커버하려면 사람 중심의 개발 방식으로는 절대 불가능했어요. 그래서 선택했습니다.

“AI가 코드를 짜고, 사람은 문제 정의와 품질 검증에 집중한다.”

우리가 사용한 AI 도구들

AI를 하나만 쓴 건 아니었습니다. 각 도구는 서로 다른 강점을 가지고 있었어요.

1️⃣ Claude Sonnet 4.5 (Claude Code) 메인 개발자 역할을 맡았어요. 테스트 코드, 유틸 함수, 리팩토링, 문서화까지 대부분을 담당했습니다. 그리고 Claude를 세 가지 페르소나로 나눠 일했습니다.

2️⃣ Cursor IDE 안의 페어 프로그래머처럼, type 에러와 import 문제를 바로바로 해결해줬습니다.

3️⃣ Codex 코드 분석과 비교에 강했습니다. “이 테스트 두 개의 차이는 무엇인가?” 같은 질문에 아주 강했어요.

AI 팀원들 – 실제로는 이렇게 일했다

🤖 SDET Agent - 테스트 설계와 아키텍처 담당

1) Page Object Model 도입 (7월 말)

테스트가 5개쯤 되었을 때, 코드가 복잡해지기 시작했습니다.

나: “셀렉터가 너무 많고 중복돼. 유지보수가 걱정돼.” SDET Agent: “Page Object Model로 바꿔볼까요?”

그리고 10분 뒤 Claude는 이런 파일을 만들어줬습니다.

export class RefundApplicationPage {
  constructor(private page: Page) {}

  async clickApplyButton() {
    await this.page.click(this.selectors.applyButton);
  }

  async fillAccountNumber(account: string) {
    await this.page.fill(this.selectors.accountInput, account);
  }

  private selectors = {
    applyButton: 'button:has-text("환급 신청")',
    accountInput: 'input[name="account"]',
  };
}

이후로 셀렉터 변경은 딱 한 곳만 고치면 끝이었어요. 일지에도 이렇게 남아 있습니다.

“중복 셀렉터 스트레스가 사라졌다.” — 8월 자동화 일지

2) 약관(동의) 플로우 자동 정리 (9월 12일)

“세금비서에서 약관 한 줄이 추가되면서 기존 자동화가 모두 멈췄어요.”

문제는 약관이 서비스마다, 유입 경로마다 다르다는 점이었습니다. 숨은환급금찾기 / 세금비서 / 현금영수증 / 더낸 연말정산 돌려받기 서비스는 약관 개수, 순서, 명칭까지 전부 달라요.

AI에게 요청했습니다.

나: “서비스별로 약관 구조를 자동으로 감지해서 처리하는 유틸을 만들어줘.”

Claude가 제안한 내용은 다음과 같아요.

결과적으로 하나의 clickInitialConsent() 함수만 유지하면 네 가지 서비스의 약관 변화가 모두 자동으로 반영되도록 만들 수 있었어요. 이후 약관 변경이 여러 차례 있었지만, 테스트는 단 한 번도 중단되지 않았습니다.

📝 Documentation Specialist - 문서와 일지 관리 담당

저녁이면 저는 AI에게 한 줄만 입력했습니다.

“오늘 커밋 기반으로 일지 정리해줘.”

그러면 다음과 같은 완성된 회고가 나옵니다.

📅 2025-11-24
  • 신규 테스트 3개 추가 (탈퇴, 홈택스 가입, 연금공제 미공제)
  • clickButton Fallback 기본 활성화
  • 월세 공제 로직 수정
  • v3.3.0 기준 테스트 35개 통과
  • 문서(CLAUDE_CONTEXT.md, QA_AUTOMATION_GUIDE.md) 최신화 완료

이전에는 30~40분씩 들여 쓰던 일지를 이제는 1~2분 검토만 하면 작성을 마칠 수 있게 되었어요.

문서로 기록을 남기니 이후에 같은 부분에서 막히면 손쉽게 해결을 할 수 있었어요. 또 한 팀원에게 전파도 쉬워졌습니다.

✍️ Git Master - 커밋·PR 코파일럿

초반에는 흔한 커밋 메시지를 사용했습니다.

git commit -m "fix"
git commit -m "update test"

하지만 지금은 대부분 이런 메시지예요.

feat(test-15): prevent duplicate userNo across tests

- ensure each test uses unique userNo
- add duplicate-check bash script
- update QA_AUTOMATION_GUIDE with isolation rule

Closes 

6개월이 지나니 커밋 히스토리만 봐도 팀의 발자국이 또렷하게 보였습니다.

💬 내부 메신저로 이어진 AI–사람–테스트 루프

AI가 코드만 짠 건 아니에요. 테스트 결과와 데이터도 내부 메신저를 통해 주고받는 구조를 만들었습니다. 매 테스트가 끝나면, 자동으로 이런 메시지가 떨어져요.

  • 결과, 실행시간, 환경, 실행시각, 테스트 UserNo(테스트 고유 ID), 관련 티켓 브랜치 등

테스트가 실패하면 메시지는 조금 달라집니다.

  • 에러메시지, 실패 단계, 실패 단계 관련 로그를 추적할 수 있도록 EventID, 실패단계 로그 등

여기서 중요하게 볼 점은 어디서 어떻게 테스트가 실패하느냐였어요. 그래서 해당 실패 스레드의 댓글로

  • 테스트 리포트
  • 테스트 실패 지점 스크린샷

이 오도록 AI에게 요청했고 반영이 되었습니다.

더 재밌는 점은 내부 메신저에서 바로 논의가 시작된다는 점이었어요.

QA: “배포 후 13번 테스트가 깨지고 있어요. 의료비 공제 쪽 확인 부탁드려요”

테스트 결과도 마찬가지였습니다. 테스트 진행 단계 및 오류 로그등 사람이 이해하기 좋은 형태의 테스트 데이터 리포트가 바로 도착하죠.

코드를 치는 시간보다 내부 메신저에서 AI와 대화하는 시간이 더 많아진 경험이었습니다.

시간순 개발 스토리

올해 7월부터 지금까지 시간순으로 개발 여정에 대해 좀 더 자세히 들여다 볼게요.

7월 — 파일럿 시작

현금영수증/의료비/신용카드 등 핵심 테스트 5개를 제작했고, Page Object Model을 도입하며 테스트 유틸 함수 틀을 완성했습니다.

8월 — 문서화 & React 타이밍 이슈 해결

스크린 상에는 버튼이 이미 렌더링되어 있는데, Playwright는 지속적으로 아래 오류를 내며 클릭을 거부했습니다.

Error: Element is not clickable

UI는 보이는데 클릭이 안 되는, React 기반 서비스에서 간헐적으로 나타나는 전형적인 플로우였어요. 문제를 추적해보니, DOM이 렌더링된 시점과 이벤트 핸들러가 바인딩되는 시점 사이에 미묘한 비동기 갭이 존재했습니다.

Playwright는 “보이는 요소”만을 기준으로 클릭을 시도하기 때문에 이벤트 핸들러가 아직 바인딩되지 않은 상태에서는 렌더는 끝났는데 상호작용은 아직 불가한 상태가 만들어졌습니다.

Claude는 먼저 문제를 이렇게 정리해줬습니다.

“React에서는 ‘보인다(Visible)’와 ‘클릭 가능(Interactable)’이 반드시 같은 시점이 아닙니다. Playwright가 클릭을 시도할 때 이벤트 핸들러가 아직 바인딩되지 않은 상태입니다. UI 안정화(visual ready)와 상호작용 준비(interaction ready)를 분리해 기다릴 필요가 있습니다.

그리고 아래와 같은 표준화된 인터랙션 준비(Interaction Readiness) 전략을 제안했어요.

1️⃣ DOM → React → 이벤트 핸들러 순서로 안정화 대기 (예시)

단순 sleep이 아니라, 단계별로 UI 안정성을 확보하는 구조입니다.

// UI가 "보이는 것"과 "상호작용 가능한 것"은 다르기 때문에
// interaction readiness를 명시적으로 보장하는 헬퍼.
export async function waitForReactInteractionReady(page: Page, selector: string) {
  // 1) DOMContentLoaded: 기본 DOM 파싱 완료
  await page.waitForLoadState('domcontentloaded');

  // 2) 요소 존재 여부 확인 (레이아웃 완료)
  await page.waitForSelector(selector, { state: 'visible', timeout: 5000 });

  // 3) React hydration + effect 바인딩까지 고려한 안정화 대기
  await page.waitForFunction(
    (sel) => {
      const el = document.querySelector(sel);
      return !!el && typeof el.onclick === 'function';
    },
    selector,
    { timeout: 8000 }
  );
}

2️⃣ 상호작용 fallback 전략 (예시)

‘한 번 클릭 → 안 되면 강제 클릭 → 그래도 안 되면 네이티브 입력(Enter)’ 같은 전략이 아니라, 위험하지 않은 순서로 정리된 fallback입니다.

export async function safeClick(page: Page, selector: string) {
  try {
    // 1. 표준 클릭
    await page.click(selector, { timeout: 3000 });
    return;
  } catch (_) {}

  try {
    // 2. 네이티브 키보드 인터랙션 (가장 안정적)
    await page.keyboard.press('Enter');
    return;
  } catch (_) {}

  // 3. 최후의 수단: JS로 디스패치
  await page.$eval(selector, (el: HTMLElement) => el.click());
}

3️⃣ 최종 결합된 Click API (예시)

export async function clickReactButton(page: Page, text: string) {
  const selector = `button:has-text("${text}")`;

  // React 이벤트 바인딩 대기
  await waitForReactInteractionReady(page, selector);

  // 안정적 클릭
  await safeClick(page, selector);
}

이 전략을 적용하면서 16번·17번 테스트에서 반복적으로 발생하던 클릭 실패는 완전히 사라졌습니다.

특히 핵심은 ‘랜덤하게 25초 기다리기’ 같은 접근이 아니라, React UI의 특성을 고려해 ‘상호작용 가능’ 상태를 직접 감지한 것이었어요. 문제 자체는 단순해 보였지만, 그 단순함의 본질까지 AI가 정확히 짚어준 순간이기도 했습니다.

9월 — 대규모 리팩토링 & 테스트 격리

2,147줄짜리 파일을 3개 파일로 분리했고, 23개 테스트 import를 자동 수정했으며, userNo 충돌 이슈를 완전히 해결했습니다. ‘자동화가 돌아간다’에서 ‘자동화 품질을 관리할 수 있다’ 단계로 올라간 시기였어요.

10월 — 약관 시스템 재설계

서비스가 여러 개로 늘어나면서 약관은 폭발적으로 복잡해졌습니다. Claude는 다음과 같이 제안했어요.

그 결과 3일만에 약관의 정합성을 확인하는 시스템이 만들어졌습니다. 이후 약관 추가는 설정 파일 한 줄 수정이면 끝나게 되었어요.

11월 — 35개 테스트 완성 & 운영 모드 진입

탈퇴, 홈택스 가입, 연금공제 미공제 테스트를 추가했고, clickButton 기본 Fallback을 활성화했으며, 문서와 코드를 동기화했습니다(v3.3.0). 이때 일지에는 이렇게 적혀 있었어요.

“이제는 테스트를 ‘추가’하는 단계가 아니라, 안정적으로 ‘운영’하는 단계에 도달했다.”


AI와 함께 일해본다는 것

5개월 동안 저는 아래 네 가지에 거의 모든 시간을 썼습니다.

코드를 직접 친 시간은 10%도 되지 않았어요. 대신 AI가 짜온 코드와 설계의 방향을 잡아주는 역할을 했죠. 그래서 스스로에게 이런 질문을 하게 됩니다.

코드를 잘 치는 사람보다 AI와 함께 문제를 해결할 수 있는 사람이 앞으로 더 중요한 건 아닐까?


AI-driven 테스트 자동화

AI가 QA를 대체할까요? 저는 그렇게 생각하지 않습니다. 하지만 AI가 QA의 역량을 증폭시키는 시대는 이미 시작됐다고 믿어요. 세금 환급처럼 복잡한 도메인에서도, AI는 충분히 팀원 역할을 해냈습니다.

그리고 지난 5개월을 거치며 저에게 남은 가장 큰 인사이트는 이것입니다.

“AI가 좋은 품질의 속도를 만든다면, QA는 그 속도가 향해야 할 ‘방향’을 만드는 사람이다.”

댓글 0댓글 관련 문의: toss-tech@toss.im
㈜비바리퍼블리카 Copyright © Viva Republica, Inc. All Rights Reserved.