배달의민족 전자계약서 화면 개편기: AI에게 맡긴 것과 사람이 챙긴 것

2026. 09. 18. 이형준

AI Frontend

들어가며

전자계약서는 파트너(사장님)가 배달의민족을 처음 시작할 때 작성하는 전자문서입니다. 영업 담당자가 파트너와 함께 작성하고, 계약 내용을 바꿀 때도 같은 문서를 씁니다. 이 문서를 쓰는 작성 화면은 사업자 정보, 가게 정보, 광고 정보 같은 섹션이 모여 계약서 한 장을 이루는 큰 폼입니다.

2026년 4월부터 6월까지 이 작성 화면을 개편했습니다. 작성자가 계약서를 더 편하게 쓸 수 있게 하는 것이 목표였습니다. 그렇게 해서 계약서 한 장을 작성하는 데 드는 시간도 줄이려 했습니다.

그런데 기존 화면과 코드 위에서는 이 일이 쉽지 않았습니다. 화면은 Bootstrap과 Ant Design 위에서 디자이너 없이 개발자가 그린 것이어서, 사내 디자인시스템을 쓰는 디자이너와 함께 만들 수 없었습니다. 코드에는 폼 값을 관리하는 체계가 없어서, 값을 어디서 바꾸고 어디까지 다시 그려야 하는지 알기 어려웠습니다. 그래서 화면과 코드 구조도 함께 바꿨습니다.

코드는 처음부터 새로 썼고, 그 대부분을 AI 코딩 도구(클로드 코드)로 만들었습니다. 이 글은 그 과정에서 무엇을 AI에게 맡기고 무엇을 직접 챙겼는지 정리한 기록입니다. 개편하는 동안 부딪힌 질문은 세 가지였습니다.

  • AI가 만든 코드가 아무 데나 생기는 것을 어떻게 막을까
  • AI에게 로직을 맡길 때 "그게 아니고"를 반복하지 않으려면 어떻게 해야 할까
  • 그래서 실제로 얼마나 빨라지고, 배포 뒤 품질은 어땠을까

같은 고민을 하는 분께 이 글이 참고가 되면 좋겠습니다.

무엇이 바뀌었나

먼저 개편 전후 화면입니다.

개편 전 전자계약서
개편 전 전자계약서
개편 후 전자계약서
개편 후 전자계약서

가장 큰 변화는 디자인입니다. 기존 화면은 외부 라이브러리로 만든 것이어서 디자이너의 작업을 그대로 옮기기도, 디자인을 고쳐 달라고 부탁하기도 어려웠습니다. 개편 후에는 사내 디자인시스템인 우아한공방을 적용했습니다. 디자이너는 이 디자인시스템을 기준으로 작업하기 때문에, 이제 처음부터 디자이너와 함께 화면을 만들 수 있습니다.

작성자가 편하게 쓸 수 있도록 편의 기능도 두 가지 더했습니다.

  • 오른쪽의 첨부 서류 모아보기: 기존에는 서류가 섹션마다 흩어져 있어서 하나씩 찾아가 확인해야 했습니다. 이제는 계약서에 첨부된 이미지를 한 곳에서 볼 수 있습니다.
  • 왼쪽의 앵커 메뉴: 기존에는 잘못 입력한 값을 찾으려면 긴 폼을 위에서부터 훑어야 했습니다. 이제는 메뉴에서 섹션 사이를 바로 이동하고, 잘못 입력한 값이 있는 섹션은 메뉴에 에러 메시지로 표시됩니다.

코드는 왜 새로 썼나

기존 코드의 문제는 폼 값을 관리하는 체계가 없다는 것이었습니다. 전자계약서 화면은 전체가 하나의 폼이라서, 어느 섹션의 값을 바꿔도 결국 같은 계약서 데이터를 고치는 일입니다. 그런데 그 데이터를 다루는 방식이 정해져 있지 않았습니다.

기존에는 계약서 전체를 담은 큰 객체 하나를 props로 내려보냈습니다. 값을 바꿀 때는 그 객체를 직접 수정하고, 컴포넌트 트리에서 상위 컴포넌트를 골라 강제로 다시 그렸습니다.

// 계약서 전체를 하나의 객체로 만들어 최상위에서 props로 내려보낸다
const contractData = new ContractData(data)

// 깊은 곳의 컴포넌트(사업자등록번호 입력란)에서 값을 바꿀 때
class BusinessNumberInput extends React.Component {
  handleChange = (e) => {
    // 1. props로 받은 객체를 직접 고친다
    this.props.contractData.document.owner.businessNumber = e.target.value

    // 2. 리액트는 이 변경을 모르므로, 상위 컴포넌트를 찾아 강제로 다시 그린다
    this.props.parent.parent.forceUpdate()
  }
}

이 방식에서는 어느 컴포넌트가 어떤 값을 바꾸는지 찾기 어렵습니다. 값을 바꾼 뒤 어디까지 올라가서 다시 그려야 하는지도 매번 사람이 판단해야 했습니다.

이런 코드가 컴포넌트마다 들어 있어서 몇 곳만 고쳐서는 체계가 생기지 않았습니다. 디자인시스템을 바꾸면서 화면 코드도 어차피 모두 새로 그려야 했으니, 고치는 것보다 새로 쓰는 쪽을 택했습니다.

코드를 세 영역으로 나눴다

새 전자계약서의 코드는 뷰(View), 폼(Form), 스토어(Store)로 나눴습니다. 뷰는 화면을 그리고, 폼은 사용자가 입력하는 값을 갖고, 스토어는 그 밖의 상태와 동작을 맡습니다. 앱 바깥에 닿는 곳은 스토어 하나입니다.

세 영역과 그 사이의 흐름은 다음과 같습니다.

뷰, 폼, 스토어 세 영역과 그 사이의 흐름

이렇게 나눈 것은 AI가 쓴 코드가 제자리를 벗어나지 않게 하기 위해서입니다. 코드의 많은 부분을 AI 코딩 도구로 쓰는데, 경계가 없으면 AI는 코드를 아무 데나 넣습니다. 뷰에서 API를 부르고, 폼에서 서버 응답을 직접 다루는 식입니다. 경계를 먼저 정해 두자 AI가 만든 코드도 그 안에 들어왔습니다.

나눈 이유는 AI였지만, 앞 섹션에서 본 기존 코드의 두 문제도 이 구조에서 풀렸습니다. 어느 컴포넌트가 값을 바꾸는지 찾기 어렵던 문제는 값을 고치는 곳이 폼 하나가 되면서 사라졌습니다. 어디까지 다시 그릴지 사람이 판단하던 문제는 react-hook-form이 필드 단위로 정해 주면서 없어졌습니다.

뷰는 받은 것을 그리고, 일어난 일을 알립니다. 값을 스스로 기억하지 않아서, 무엇을 보여 줄지는 늘 밖에서 정해 줍니다.

function ConsultNumberView({ value, onChange, invalid, statusMessage, disabled, ref }: Props) {
  return (
    <Field label="상담번호" invalid={invalid} statusMessage={statusMessage}>
      <Input
        ref={ref}
        value={value}
        onChange={(e) => onChange(e.target.value)}
        invalid={invalid}
        disabled={disabled}
      />
    </Field>
  );
}

뷰는 값이 어디서 왔는지, 어디로 가는지 모릅니다. 그래서 값만 넣어 주면 앱 없이도 화면에 뜹니다. 이렇게 컴포넌트 하나만 따로 띄워 보는 도구가 스토리북입니다. 디자이너와 화면을 맞출 때는 계약서 전체를 열지 않고, 스토리북에 뜬 뷰 하나만 놓고 봅니다.

폼은 뷰와 스토어를 잇는 자리입니다. 스토어에서 필요한 값을 읽고, 사용자가 입력하는 값을 갖고, 그 둘을 뷰에 넘깁니다. 입력란마다 컴포넌트가 하나씩 있습니다. 입력란 컴포넌트가 모여 섹션 컴포넌트가 되고, 섹션들이 루트 하나에 묶여 계약서 폼이 됩니다. 아래는 입력란 컴포넌트 하나의 예시입니다.

function ConsultNumber() {
  // ① 스토어에서 읽는다
  const canView = useContractStore((s) => s.permissions.canViewConsultNumber);

  // ② 폼에 등록한다. 값도, 검증 규칙도 여기
  const consultNumber = useField('consultNumber', {
    required: '상담번호를 입력하세요',
  });

  if (!canView) return null;

  // ③ 뷰에 넘긴다. 값, 바꾸는 방법, 에러 표시, 포커스를 줄 ref
  return (
    <ConsultNumberView
      ref={consultNumber.ref}
      value={consultNumber.value}
      onChange={consultNumber.setValue}
      invalid={!!consultNumber.error}
      statusMessage={consultNumber.error?.message}
    />
  );
}

스토어

스토어는 상태와 동작을 갖습니다. 리액트에 기대지 않는 자바스크립트 코드라서, 컴포넌트나 훅 없이 혼자 돌아갑니다. 앞의 폼 예시에서 읽은 권한 값은 아래 동작이 채워 넣습니다.

function createContractStore(deps: ContractStoreDeps) {
  return createStore((set) => ({
    // 서버에서 문서를 받아 권한을 채운다
    loadDocument: async (id) => {
      const doc = await deps.api.fetchDocument(id);
      set({ permissions: doc.permissions });
    },
  }));
}

서버를 직접 부르는 코드는 스토어 안에 없습니다. 스토어를 만들 때 필요한 함수들을 deps라는 인자로 받아, 그것을 부르기만 합니다. 어떤 함수를 넣을지는 스토어를 만드는 쪽이 정하는, 이른바 의존성 주입입니다.

이렇게 한 것은 서버 없이 스토어와 폼을 테스트하기 위해서입니다. 서버를 부르는 코드가 스토어 안에 박혀 있으면, 권한이 없는 문서 같은 경우를 확인하려고 서버 데이터를 그 상태로 맞춰야 합니다. 함수를 밖에서 넣게 하면, 테스트에서는 원하는 응답을 돌려주는 가짜 함수를 넣으면 됩니다.

만든 스토어는 프로바이더로 폼 트리에 넣습니다. 폼 안의 컴포넌트는 useContractStore라는 훅으로 그 스토어를 꺼내 씁니다. 앞의 폼 예시에서 권한 값을 읽을 때 쓴 것이 이 훅입니다.

// 앱: 진짜 API를 넣는다
const store = createContractStore({ api: RealAPI });

<ContractFormProvider contractStore={store}>
  <ContractForm />
</ContractFormProvider>

테스트에서는 가짜 API를 넣습니다. 권한이 없는 문서를 돌려주게 하면, 상담번호 입력란이 보이지 않아야 합니다.

// 테스트: 가짜 API를 넣는다
const fakeAPI = {
  fetchDocument: async () => ({ permissions: { canViewConsultNumber: false } }),
};
const store = createContractStore({ api: fakeAPI });

render(
  <ContractFormProvider contractStore={store}>
    <ContractForm />
  </ContractFormProvider>,
);
await store.getState().actions.loadDocument('1');

expect(screen.queryByLabelText('상담번호')).toBeNull();

앱과 테스트의 차이는 프로바이더에 넣는 스토어 하나뿐입니다.

경계는 도구가 지킨다

뷰, 폼, 스토어 사이의 의존 관계는 사람이 기억하는 대신 린트와 테스트가 지키게 했습니다.

한 파일 안에서 판단할 수 있는 것은 린트가 봅니다. 영역마다 규칙을 만들어서, 뷰에서 상태 훅을 쓰거나, 폼에서 API를 직접 부르거나, 스토어에서 브라우저 전역 객체를 읽으면 실패합니다.

파일 하나만 봐서는 알 수 없는 것은 테스트가 봅니다. 폴더 구조가 규칙에 맞는지, 영역 전체가 린트를 통과하는지 같은 것들입니다.

뷰는 AI가 그린다

세 영역 중 뷰부터 AI에게 맡겼습니다. 입력과 결과가 모두 눈에 보이기 때문입니다. 뷰는 상태가 없고 다른 코드에 의존하지 않으며, 입력은 디자인 이미지 하나입니다. 결과도 스토리북에 띄워 눈으로 확인할 수 있습니다.

과정이 매번 같아서, 뷰를 만드는 순서를 스킬 하나로 묶었습니다. 스킬은 AI가 따를 작업 절차를 적어 둔 문서입니다. 디자인 이미지와 뷰 이름을 주면 AI가 이미지를 보고 어떤 컴포넌트를 쓸지 정하고, 뷰 파일을 만들고, 스토리북에 띄워 디자인과 비교합니다.

디자인 이미지로 뷰를 요청하면 AI가 계획을 세우고, 영역마다 어떤 컴포넌트를 쓸지 사람에게 묻는다

디자인시스템을 AI가 읽을 수 있게 만들기

스킬을 만들기 전에 먼저 디자인시스템 문서를 저장소 안으로 가져왔습니다. AI가 컴포넌트를 고르려면 읽을 것이 있어야 하기 때문입니다. 문서 사이트 주소와 저장 형식만 정하면 AI가 브라우저를 열어 컴포넌트 문서를 하나씩 읽고, 설명과 props 표는 마크다운으로, 렌더링된 모습은 이미지로 저장합니다. 컴포넌트 59개가 이렇게 파일이 됐습니다.

design-system-docs/blue/
├── _index.md        # 59개 컴포넌트 목록과 한 줄 설명
├── Button.md        # 개요, props 표, 사용 예시
├── Button.png       # 실제 렌더링 모습
├── TextInput.md
├── TextInput.png
└── ...

어떤 컴포넌트를 쓸지 고르기

디자인 이미지를 주면 AI가 화면을 영역으로 나누고, 영역마다 어떤 컴포넌트를 쓸지 위 문서에서 찾아 제안합니다. 추천 하나와 대안을 함께 내되, 문서에 없는 컴포넌트는 제안하지 못하게 했습니다.

고르는 것은 사람입니다. 컴포넌트를 잘못 고르면 뷰를 다 만든 뒤에 갈아엎어야 해서, 만들기 전에 확정하는 쪽이 손이 덜 듭니다. 제안을 보고 영역마다 하나씩 확정하면 AI가 그 컴포넌트로 뷰를 만듭니다.

스스로 확인하며 고치기

뷰를 만든 뒤 AI가 스토리북을 띄우고 스크린샷을 찍습니다. 그 스크린샷을 처음 받은 디자인 이미지와 비교해서 레이아웃, 색, 간격의 차이를 찾습니다. 차이가 있으면 코드를 고치고 다시 찍습니다.

이 반복은 최대 다섯 번으로 정했습니다. 그 뒤에도 남은 차이는 사람에게 보고하고 멈춥니다. 다섯이라는 숫자는 몇 번 해 보고 정했습니다. 그 이상은 고쳐지는 것보다 드는 시간이 더 컸습니다.

사람이 마지막에 보는 것

AI가 작업을 마치면 사람이 두 가지를 봅니다.

  • 뷰의 인터페이스: props가 화면 요구사항에 맞게 나뉘어 있는지, 폼에서 넘겨줄 값과 콜백만 받는지 살핍니다.
  • 인터랙션: 스토리북에서 직접 입력하고 눌러 보며 의도한 대로 움직이는지 확인합니다. 스크린샷 비교로는 모양만 맞출 수 있고, 동작은 사람이 만져 봐야 알 수 있습니다.

다섯 번 반복 뒤에도 남는 차이는 대개 색이나 글꼴처럼 작은 것들이고, 이것도 사람이 마무리합니다. 그래도 컴포넌트 구조와 레이아웃은 AI가 잡아 두니, 처음부터 그리는 것보다 시간이 덜 듭니다.

여러 뷰를 동시에 맡길 수 있는 것이 시간을 더 크게 줄였습니다. 뷰는 서로 의존하지 않아서, 하나가 스크린샷을 비교하며 고치는 동안 다른 뷰를 시작할 수 있습니다.

폼과 스토어는 명세부터 쓴다

폼과 스토어는 무엇을 만들지 먼저 글로 정했습니다. 뷰와 달리 디자인 이미지 같은 눈에 보이는 입력이 없고, 결과도 화면으로 확인할 수 없기 때문입니다.

흔히 실패하는 두 가지

AI에게 로직을 맡기면 흔히 두 가지 방식으로 실패합니다.

하나는 하고 싶은 일을 말로 설명하고 바로 구현을 맡기는 경우입니다. AI는 부족한 정보로 먼저 코드를 만들고, 사람은 "그게 아니고"를 반복합니다. 대화가 길어질수록 앞서 잘못 만든 코드와 설명이 쌓여서 결과가 나빠집니다.

다른 하나는 계획을 먼저 받지만, 길어서 읽다가 그냥 승인하는 경우입니다. 확인하지 않은 계획대로 구현이 끝나고, 고치는 일은 그 뒤에 시작됩니다. 결과는 첫 번째와 같아집니다.

두 경우의 문제는 같습니다. 사람이 확인할 것이 한 번에 많이 오거나, 구현이 끝난 뒤에야 옵니다. 그래서 확인할 것을 작게 나누고, 확인 시점을 구현 앞으로 옮겼습니다.

파일마다 명세와 테스트를 짝으로 둔다

구현 파일 하나에 명세 파일 하나, 테스트 파일 하나를 같은 이름으로 같은 폴더에 둡니다. 이 구조도 테스트로 지킵니다. 폼과 스토어의 구현 파일 중 짝이 빠진 것이 있으면 테스트가 실패합니다.

form/
├── ConsultNumber/
│   ├── index.tsx          # 구현
│   ├── index.spec.md      # 명세
│   └── index.test.tsx     # 테스트
├── BusinessNumber/        # 사업자등록번호
│   ├── index.tsx
│   ├── index.spec.md
│   └── index.test.tsx
└── ...

파일 수는 세 배가 됩니다. 그래도 구현, 명세, 테스트를 하나씩 짝지어 둔 것은 한 번에 볼 범위를 좁히기 위해서입니다. 파일 하나를 고칠 때 볼 것은 그 파일의 명세와 테스트뿐이고, 명세가 구현과 어긋났는지 볼 때도 그렇습니다. 명세는 쓰는 것보다 구현과 맞게 유지하는 것이 어렵습니다. 한번 어긋나면 그 뒤로는 아무도 보지 않게 됩니다. 대조할 범위가 파일 쌍 하나면 사람도 AI도 자주 맞춰 볼 수 있습니다.

세 파일이 짝을 이루는 모습은 다음과 같습니다.

구현 파일 하나에 명세 파일과 테스트 파일이 같은 이름으로 짝을 이룬다

명세 파일은 목적, 현재 동작, 추가하거나 바꿀 동작, 제약 네 부분으로 되어 있습니다.

# ConsultNumber

## 목적
상담번호 입력란. 권한과 문서 종류에 따라 보이거나 막힌다.

## 현재 동작
- 권한 있는 사용자만 다뤄야 하므로, 상담번호 권한이 없으면 렌더링하지 않는다.
- 상담번호는 숫자만 허용되므로, 입력에서 숫자 외 문자를 지운 값으로 폼을 갱신한다.
- 반려된 뒤 다시 요청한 문서는 상담번호가 외부에서 정해지므로, 입력을 막는다.

## 추가하거나 바꿀 동작
- [ ] 상담 시스템과 연동되지 않은 문서도 상담번호를 바꿀 수 없으므로, 입력을 막는다.

## 제약
- 입력이 막힌 상태에서는 채울 수 없으므로, 필수 검증을 하지 않는다.

명세는 AI가 씁니다. 사람은 하고 싶은 일을 말하고, AI가 그것을 명세의 줄로 옮깁니다. 줄마다 동작과 함께 의도를 적게 했습니다. "입력을 막는다"만 쓰면 테스트 코드와 다를 게 없지만, "외부에서 정해지므로 입력을 막는다"라고 쓰면 나중에 고칠 때 왜 그랬는지 알 수 있습니다. 코드만 보고 의도를 알 수 없을 때는 AI가 추측하지 않고 사람에게 묻게 했습니다.

테스트는 명세의 동작과 제약을 1:1로 옮긴 것입니다. 현재 동작 한 줄이 테스트 케이스 하나, 제약 한 줄이 테스트 케이스 하나입니다. 추가하거나 바꿀 동작도 테스트 파일에 먼저 자리를 잡습니다. 아직 구현이 없으니 통과하지 못하는 채로 있습니다.

명세, 승인, 테스트, 승인, 구현

기능을 바꿀 때 AI는 이 순서로만 움직입니다. 명세, 테스트, 구현을 모두 AI가 쓰고, 사람은 단계 사이에서 확인만 합니다.

명세 파일 변경 → 승인 ① → 테스트 파일 변경 → 승인 ② → 구현 파일 변경 → 테스트 통과까지 반복

승인 전에는 다음 단계 파일을 건드리지 않습니다. 승인은 두 번이고 둘 다 짧습니다. 첫 번째는 명세의 추가하거나 바꿀 동작에 새로 들어간 몇 줄, 두 번째는 테스트 케이스 이름 목록입니다. 구현 코드는 이 둘을 기준으로 확인하면 됩니다.

마지막 단계에서 테스트는 AI에게 돌아가는 피드백이 됩니다. 구현이 틀리면 테스트가 실패하고, AI는 그 결과를 보고 통과할 때까지 고칩니다.

명세가 남기는 것

명세에는 그 파일이 무엇을 하는지와 왜 그렇게 하는지가 남습니다. 나중에 고칠 때 AI는 코드가 아니라 명세를 먼저 읽고, 거기 적힌 의도를 기준으로 바꿉니다. 대화가 길어져 꼬이면 새로 시작하면 됩니다. 명세가 있으니 앞선 대화의 잘못된 시도를 끌고 다니지 않습니다.

무엇이 바뀌는지는 명세 파일의 diff로 봅니다. 개발 중에는 추가하거나 바꿀 동작에 새로 들어간 줄이 이번 작업의 범위입니다. 구현이 끝나면 그 줄은 현재 동작으로 옮깁니다.

명세 파일의 diff. 추가하거나 바꿀 동작에 세 줄이 새로 들어가 있다

코드 리뷰에서도 같습니다. 다른 사람의 변경을 볼 때 현재 동작의 diff를 먼저 읽으면, 구현 코드를 열기 전에 무엇을 왜 바꿨는지 알 수 있습니다.

생산성은 얼마나 달라졌나

구현 속도는 약 2배 빨라졌고, 테스트 코드는 약 8배 늘었습니다.

비교 대상은 제가 AI 없이 참여했던 입점신청 화면 개편입니다. 입점신청은 파트너가 영업 담당자 없이 혼자 입점할 때 쓰는 문서로, 전자계약서와 성격이 비슷합니다. 개편 작업의 성격도 같았습니다. 둘 다 UI를 개선하면서 내부 구현을 우아한공방과 react-hook-form으로 다시 쓰는 일이었습니다.

두 작업 모두 제 커밋만 셌습니다. 제가 맡은 영역은 다음과 같습니다.

  • 입점신청(AI 없이): 가게 정보, 메뉴 정보, 광고 정보
  • 전자계약서(AI 활용): 사업자 정보(영업신고증 제외), 광고 정보, 첨부 서류 모아보기
입점신청(AI 없이) 전자계약서(AI 활용) 배수
기간 2024년 11월 ~ 2025년 2월 2026년 4월 ~ 2026년 6월
근무일 64일 42일
커밋 수 940 930
하루에 추가한 줄 수 702 3,278 4.7
하루에 만든 컴포넌트 수 5.2 5.1 1.0
하루에 쓴 프로덕션 로직 줄 수 605 1,224 2.0
프로덕션 1줄당 테스트 0.15줄 1.28줄 8.5
테스트 파일 78개 190개
스토리북 파일 0개 157개

하루는 근무일 기준입니다. 배수는 기간 차이에 영향받지 않는 항목에만 적었습니다.

줄 수는 과장된 숫자입니다. AI는 코드를 길게 쓰는 경향이 있고, 여기에는 테스트도 들어 있습니다. 반대로 컴포넌트 수는 축소된 숫자입니다. 전자계약서의 컴포넌트 파일은 평균 줄 수가 입점신청의 1.8배입니다. 컴포넌트 하나에 더 많은 코드가 들어가서, 하루에 만든 개수만 보면 늘지 않은 것처럼 보입니다. 컴포넌트 밖의 훅과 헬퍼로 뺀 로직도 개수에 잡히지 않습니다. 그 둘을 걸러낸 것이 프로덕션 로직 기준이고, 여기서 2배가 나옵니다. 프로덕션 로직은 테스트, 명세, 스토리북 파일을 뺀 실제 동작 코드입니다.

배포 뒤에는 어땠나

속도는 빨라졌지만, 배포 뒤 2주 동안 동작 버그는 네 건으로 입점신청 때의 두 건보다 많았습니다. 네 건은 모두 첫 주에 몰렸습니다.

비교 범위는 앞의 생산성 비교와 같습니다. 문구, 순서, 아이콘 같은 화면 표시 수정은 빼고, 동작에 문제가 있는 것만 셌습니다.

2주가 지난 뒤로는 잦아들었습니다. 12주까지 나온 동작 버그는 두 건이었고, 6주 뒤로는 한 건도 없었습니다. 입점신청은 3주에 한 건이 더 있었고, 그 뒤로는 없었습니다.

배포 뒤 12주 동안 주별 동작 버그 수정 건수. 입점신청은 2주에 2건. 전자계약서는 1주에 4건이 몰렸고, 4주에 2건, 6주부터 8주까지 각 1건, 그 뒤 0건

코드보다 사람이 먼저 한계에 닿았다

코드는 여전히 봐야 했다

개편 초기에는 코드를 보지 않고 개발했다가, 구현에서 줄인 시간을 디버깅에서 다시 썼습니다. 개편하던 2026년 4월과 5월, 제 주변에서는 더 이상 코드를 볼 필요도, 이해할 필요도 없다는 이야기가 많았고, 저도 호기심에 따라 해 봤습니다. 처음에는 괜찮았습니다. 명세, 테스트, 구현이 하나씩 짝지어 있으니 어떤 기능이 있는지는 명세만 봐도 충분했습니다.

문제는 기능이 한 곳에만 있지 않을 때 생겼습니다. 프론트엔드에는 사용자가 쓰기 편하도록 같은 기능을 여러 자리에 두는 일이 많습니다. 한 기능을 설정하는 자리에서 다른 기능도 같이 설정하게 하거나, 입력 경로는 여러 개인데 거치는 함수는 하나인 경우입니다.

전자계약서의 서류 첨부가 그랬습니다. 첨부 서류 모아보기에서도 올릴 수 있고, 각 섹션의 자기 자리에서도 올릴 수 있습니다. 올리는 방식도 파트너에게 이미 받아 둔 서류를 불러오는 것과 새 파일을 직접 올리는 것으로 갈립니다. 직접 첨부는 서류 종류마다 용량 제한, 허용 형식, 이미지 크기가 다릅니다. 첨부된 서류에 OCR(문자 인식)을 적용할지, 적용한다면 이미 입력된 값 중 어느 것을 바꿀지도 정해야 합니다.

이런 부분을 고칠 때 코드를 보지 않으니 어디와 이어지는지 따라가기 어려웠습니다. 한 곳을 고쳤는데 다른 진입점에서는 바뀌지 않은 경우가 많았고, 그때마다 디버깅이 오래 걸렸습니다. 결국 며칠을 들여 그때까지 만들어진 코드를 정리하고 함수 호출 흐름을 따라 읽었습니다. 그 뒤로 중요한 곳은 꼭 코드를 직접 확인합니다.

사람이 계속 봐야 하는 것

같은 기능이 두 벌로 갈라지는 것을 막는 일은 아직 사람이 챙깁니다. 앞에서 한 곳을 고쳤는데 다른 곳이 안 바뀌던 이유도 이것이었습니다. AI는 요청받은 자리에 코드를 만들 뿐, 비슷한 코드가 다른 곳에 이미 있는지는 묻지 않으면 확인하지 않았습니다. 그래서 진입점마다 비슷한 로직이 하나씩 생겼습니다.

막는 방법은 두 가지입니다. 타입 시스템을 활용해서 한쪽만 고치면 정적 검사에서 드러나게 하거나, 공통 함수로 묶어 고칠 곳을 하나로 만드는 것입니다. 어느 쪽이든 변경 하나가 다른 곳에 어떤 영향을 주는지 넓게 봐야 합니다.

이 일은 AI에게 맡기기 쉽지 않습니다. 이번 작업에서 AI는 적게 읽을수록 잘했습니다. 이 글에서 경계를 나누고 명세를 파일마다 둔 것도 모두 적게 읽어도 되게 하려는 장치였습니다. 그런데 이 일은 반대로 넓게 읽어야 합니다.

생산성과 안정성

속도도 얻었지만, 더 크게 얻은 것은 구조였습니다. 테스트와 스토리북처럼 프로덕션이 아닌 자산이 함께 생기는 구조가 됐습니다. 사람이 손으로 할 때는 뒤로 밀리던 일이 기본이 된 것입니다. 반면 속도는 생각만큼 몇 배로 늘리기 어려웠습니다. 코드가 나오는 속도보다 사람이 그 코드를 이해하는 속도가 먼저 한계에 닿았습니다.

돌아보면 이 글에서 정한 것들은 그 병목을 위한 장치이기도 했습니다. 뷰, 폼, 스토어로 나누고 린트로 지킨 이유 중 하나는 제가 바로 알아볼 수 있는 구조 안에서만 AI가 코드를 쓰게 하려는 것이었습니다. 구현보다 명세를 먼저 쓰게 한 것도 코드를 읽기 전에 무엇을 하는지 알 수 있게 하려는 뜻이 있었습니다.

같은 시간에 만들어지는 코드가 늘어난 만큼 봐야 할 코드도 늘었습니다. 명세가 있어서 무엇을 하는 코드인지는 빨리 알 수 있지만, 코드를 한 줄씩 꼼꼼히 읽는 일은 자연스럽게 줄었습니다. 속도를 얻으면서 안정성을 같이 지키는 방법은 AI를 더 잘 쓰기 위해 계속 찾아가야 할 숙제라고 생각합니다.

  • 우아한형제들 파트너셀프서비스팀에서 웹 프론트엔드 개발자로 일하고 있습니다.