Devin · Windsurf

Devin이 만든 PR, 테스트 통과만 보고 합쳐도 될까?

DevPilot 2026. 9. 24. 00:19

같은 함수가 테스트 두 개는 모두 통과했습니다. 그런데 로딩 중과 네트워크 오류를 넣자 둘 다 “검색 결과 없음”으로 판정했어요. 테스트는 거짓말하지 않았습니다. 그 두 상황을 묻지 않았을 뿐입니다.

Devin의 PR을 합치기 전에는 요청한 범위, 실제 변경, 검증한 조건을 대조합니다. 이번 설명용 예제에서 결함 함수는 정상 응답만 검사하면 2/2 통과했지만, 로딩·오류를 더하면 2/4만 통과했습니다. 발견한 문제를 어떤 수정 요청으로 돌려보낼지도 이어서 적었습니다.

Devin이란? 요금과 한계 정리에서는 검색 화면 한 부분을 맡기는 요청서를 만들었습니다. 이번에는 그 요청의 결과를 검토하는 쪽으로 넘어갑니다.

2026-09-24 작성. Node.js v22.12.0에서 실행한 독립 JavaScript 실습입니다. 결함은 설명을 위해 의도적으로 넣었으며, 실제 Devin이 만든 코드나 사용 후기가 아닙니다. 그림도 설명용 도식입니다.

처음 볼 것은 완료 보고보다 변경 파일 목록입니다

앞 글에서 맡긴 일은 “검색이 성공했는데 결과가 없으면 안내 문구를 표시하기”였습니다. 수정 범위도 검색 화면과 관련 테스트로 제한했어요. 그렇다면 PR을 열고 그 범위부터 맞춰볼 수 있습니다.

예를 들어 아래 목록이 보인다면, 저는 마지막 두 파일이 왜 바뀌었는지 먼저 묻겠습니다. 설명을 위한 가상의 파일 목록입니다.

검색 화면 파일
검색 화면 테스트
공통 HTTP 요청 처리 파일   ← 변경 이유 확인
package.json             ← 의존성 변경 이유 확인

패키지를 추가해야 하는 작업도 있고, 공통 요청 처리에 원인이 있을 수도 있습니다. 파일 이름만 보고 잘못된 수정이라고 판단할 수는 없어요. 다만 “빈 결과 안내 추가”에 필요한 변경인지, 요청보다 범위가 커진 것인지는 설명이 있어야 합니다.

이때 보낼 질문: 공통 HTTP 처리와 의존성을 바꾼 이유를 설명해줘. 검색 화면 수정에 꼭 필요한 부분과 별도로 처리할 수 있는 부분을 나눠줘.

변경 줄 수보다 다른 기능에 닿는 범위를 먼저 보는 겁니다. 화면 한 곳을 고치려다 공통 오류 처리를 바꾸면, 검색 밖에서도 동작이 달라질 수 있으니까요.

“결과 0개”와 “아직 결과를 못 받음”은 다릅니다

이제 검색 화면이 표시할 상태를 고르는 함수만 떼어 봅니다. 입력은 status와 결과 배열 items, 출력은 empty·results·loading·error 중 하나입니다.

function brokenView({ status, items }) {
  if (items.length === 0) return 'empty';
  if (status === 'loading') return 'loading';
  if (status === 'error') return 'error';
  return 'results';
}

첫 줄은 그럴듯해요. 배열이 비었으니 빈 결과 화면을 고릅니다. 하지만 요청을 기다리거나 실패한 상태에서도 배열은 비어 있을 수 있습니다. 첫 번째 return을 만나면 아래의 로딩·오류 검사는 실행되지 않아요.

여기서 status: 'success'인 입력만 검사하면 두 조건이 모두 통과합니다. 빈 배열에는 empty, 항목이 있는 배열에는 results를 돌려주니까요. 문제를 찾으려면 “원하는 안내가 뜨는가?”에서 한 발 더 가야 합니다.

그 안내가 뜨면 안 되는 순간도 확인했나요?

검사 조건을 그대로 늘리자 실패가 드러났습니다

이번 예제의 규칙은 단순합니다. 검색이 성공한 뒤에만 결과 개수로 화면을 정합니다. 로딩 중이면 로딩 화면, 실패했다면 오류 화면을 유지합니다.

성공 응답의 두 조건에 로딩·오류를 더했습니다. 네 조건으로 결함 함수와 수정 함수를 각각 실행한 실제 출력입니다.

broken / success-only
  PASS success-empty: empty
  PASS success-results: results
  passed=2/2

broken / all-four
  PASS success-empty: empty
  PASS success-results: results
  FAIL loading-empty: expected=loading, actual=empty
  FAIL error-empty: expected=error, actual=empty
  passed=2/4

fixed / all-four
  PASS success-empty: empty
  PASS success-results: results
  PASS loading-empty: loading
  PASS error-empty: error
  passed=4/4

DEMO OK: expected failures reproduced; fixed function passed all four.

loading-empty와 error-empty에서만 예상값과 실제값이 다릅니다. “안내 문구가 안 나온다”가 아니라 “로딩·오류 상태에서도 빈 결과로 분류한다”라고 문제를 좁힐 수 있습니다.

수정은 상태를 먼저 구분하는 것입니다

function fixedView({ status, items }) {
  if (status === 'loading') return 'loading';
  if (status === 'error') return 'error';
  return items.length === 0 ? 'empty' : 'results';
}

테스트의 기대값은 바꾸지 않았습니다. 같은 네 조건을 두 함수에 넣었을 때, 수정 함수는 4/4를 통과했습니다. 정상 동작을 확인한 두 테스트를 지우는 대신, 그동안 빠진 두 조건을 더한 결과입니다.

전체 실습 코드 펼치기: 복사해서 실행

빈 작업 폴더에 아래 코드를 review-example.mjs로 저장하세요. Node.js가 필요하며 별도 패키지를 설치하지 않습니다.

node review-example.mjs
// Explanatory fixture. This is not output from an actual Devin session.
import assert from 'node:assert/strict';

// Bug: an empty array is treated as a successful empty response.
function brokenView({ status, items }) {
  if (items.length === 0) return 'empty';
  if (status === 'loading') return 'loading';
  if (status === 'error') return 'error';
  return 'results';
}

// Contract: only a successful response can show an empty-results message.
// Inputs here have status loading/error/success and an items array.
function fixedView({ status, items }) {
  if (status === 'loading') return 'loading';
  if (status === 'error') return 'error';
  return items.length === 0 ? 'empty' : 'results';
}

const cases = [
  ['success-empty', { status: 'success', items: [] }, 'empty'],
  ['success-results', { status: 'success', items: ['book'] }, 'results'],
  ['loading-empty', { status: 'loading', items: [] }, 'loading'],
  ['error-empty', { status: 'error', items: [] }, 'error'],
];

function check(label, view, selected) {
  let passed = 0;
  console.log(label);
  for (const [name, input, expected] of selected) {
    const actual = view(input);
    try {
      assert.equal(actual, expected);
      passed += 1;
      console.log(`  PASS ${name}: ${actual}`);
    } catch (error) {
      if (!(error instanceof assert.AssertionError)) throw error;
      console.log(`  FAIL ${name}: expected=${expected}, actual=${actual}`);
    }
  }
  console.log(`  passed=${passed}/${selected.length}\n`);
  return passed;
}

// Expected failures are part of this demonstration, not a CI test suite.
assert.equal(check('broken / success-only', brokenView, cases.slice(0, 2)), 2);
assert.equal(check('broken / all-four', brokenView, cases), 2);
assert.equal(check('fixed / all-four', fixedView, cases), 4);
console.log('DEMO OK: expected failures reproduced; fixed function passed all four.');

이 스크립트는 결함 함수의 예상된 실패까지 재현해야 성공으로 끝납니다. 그래서 중간에 FAIL이 있어도 마지막에 DEMO OK가 나오면 종료 코드는 0입니다. 실제 서비스의 CI 테스트 파일로 그대로 사용하기 위한 코드는 아닙니다.

Devin에는 실패 조건과 유지할 동작을 함께 보냅니다

실패를 찾은 다음에는 “테스트 다시 해줘” 대신 재현 입력과 기대 결과를 전달합니다. 아래 요청문은 방금 예제에 맞춘 것입니다. 실제 저장소에서는 파일 경로와 실행 명령을 바꿔 넣으세요.

검색 상태를 판정하는 함수에서 아래 두 입력이 잘못 처리됩니다.

재현
- status=loading, items=[] → 현재 empty / 기대 loading
- status=error, items=[] → 현재 empty / 기대 error

수정 기준
빈 결과 안내는 검색 성공 후 결과가 0개인 경우에만 표시합니다.
기존 success-empty, success-results 동작은 유지합니다.
로딩·오류 테스트의 기대값을 empty로 바꾸지 마세요.

완료 보고
수정한 파일과 이유, 실행 명령, 네 조건의 결과를 적어주세요.
브라우저에서 확인하지 못했다면 별도로 표시해 주세요.
공통 요청 처리나 의존성 변경이 필요하면 이유를 먼저 설명해 주세요.
수정 커밋 기준으로 검증하고, 병합은 하지 마세요.

특히 “테스트를 통과하도록 고쳐줘”만 보내면 무엇을 정답으로 삼을지가 빠집니다. 구현을 바꿀 것인지, 기대값이 잘못됐는지를 구분해야 해요. 이 예제에서는 처음 정한 요구사항이 로딩·오류 유지이므로 기대값을 empty로 바꾸면 안 됩니다.

재검토 시점: 수정 커밋이 추가되면 변경 내용과 테스트 결과를 다시 확인합니다. 이전 커밋의 통과 결과를 새 커밋의 근거로 그대로 쓰지 않습니다.

Devin Review를 쓰더라도 요구사항은 남겨야 합니다

Devin Review 공식 문서는 변경 묶음 정리, 버그 지적, 코드 맥락을 참고한 질문 기능을 안내합니다. 반복해서 확인할 규칙은 저장소의 REVIEW.md에 적어 리뷰 문맥으로 제공할 수도 있습니다.

검색 화면을 자주 고치는 프로젝트라면 다음처럼 구체적인 규칙을 남길 수 있겠습니다. 이것도 작성 예시이며, 이 글에서 Devin Review에 적용해 시험한 것은 아닙니다.

# 검색 화면 리뷰 기준
- 빈 결과 안내는 성공 응답의 결과가 0개일 때만 표시한다.
- 로딩·오류를 빈 결과로 바꾸는 수정이 있는지 확인한다.
- 공통 HTTP 처리 또는 의존성 변경이 있으면 필요성을 확인한다.
- 테스트 기대값 변경이 요구사항 변경에 근거하는지 확인한다.

리뷰 도구가 변경을 이해하는 데 도움을 줄 수는 있습니다. 그래도 지적이 없다는 사실과 요구사항이 모두 검증됐다는 사실은 다릅니다. 요청서에 빠진 정책이나 테스트하지 않은 환경이 무엇인지는 따로 확인해야 합니다.

4/4 통과 뒤에도 화면 확인은 남아 있습니다

여기서 실행한 것은 화면 상태를 고르는 함수입니다. 실제 버튼이나 안내 문구가 브라우저에 표시되는지, 네트워크 실패가 이 함수의 error 입력으로 전달되는지까지 확인한 것은 아닙니다.

  • 연결: 요청 실패를 상위 코드가 정상 빈 배열로 바꿔 넘기지 않는지 확인합니다.
  • 화면: 로딩·오류·빈 결과·목록이 의도한 시점에 나타나는지 봅니다.
  • 범위: 함수 밖에서 인증·API 형식·의존성이 달라졌다면 해당 영향도 확인합니다.

또 이 실습은 세 가지 상태값과 배열 입력을 전제로 합니다. 첫 검색 전 상태, 이전 결과를 유지하는 재검색, 늦게 도착한 응답, 잘못된 입력은 다루지 않았어요. 그런 동작이 있는 서비스라면 완료 조건도 달라집니다.

병합 전에 남길 근거: 요청 범위와 변경 파일이 맞는지, 실패하던 조건이 수정됐는지, 기존 동작이 유지되는지, 무엇을 아직 확인하지 못했는지. 테스트 숫자는 그 근거 중 하나입니다.

저라면 “4개 통과”보다 “어떤 네 개였는지”를 PR 설명에 남기겠습니다. 테스트 결과를 합격 도장처럼 쓰기보다, 검토한 범위를 표시하는 기록으로 읽는 편이 다음 판단에 도움이 됩니다.

자주 묻는 질문

테스트가 모두 통과하면 병합해도 되나요?

검사한 조건이 무엇인지 먼저 확인해야 합니다. 이번 결함 함수도 정상 응답 두 조건만 검사하면 모두 통과했습니다. 변경 범위와 빠진 상황, 실제 화면 연결까지 검토한 뒤 판단합니다.

이 예제는 Devin이 실제로 만든 코드인가요?

아닙니다. 검토 과정을 설명하려고 결함을 의도적으로 넣은 독립 JavaScript 예제입니다. Node.js에서 직접 실행했지만 Devin의 성능이나 성공률을 측정한 것은 아닙니다.

실습 출력에 FAIL이 있는데 종료 코드는 왜 0인가요?

결함 함수에서 예상된 두 실패를 재현하고 수정 함수가 네 조건을 통과하는지 확인하는 데모이기 때문입니다. 일반 CI에서는 실제 검사 실패를 실패 종료 코드로 전달해야 합니다.

함께 보면 좋은 글

처음 작업을 맡기기 전이라면 Devin의 역할과 첫 요청 예제부터, 수정 방향이 자꾸 어긋난다면 방향 주는 지시법을 이어서 보세요.

참고 자료와 실행 범위

실행 환경: macOS, Node.js v22.12.0. 독립 함수의 반환값을 확인했습니다. 실제 Devin 세션, 원격 PR·CI, 브라우저 화면, 배포 환경은 검증하지 않았습니다.