포매터가 build:cache:hit를 cache로 끝나는 메모로 만듭니다. 다른 입력은 공백이 두 칸씩 남습니다. 의도한 동작인지 알려주는 테스트가 없습니다. AI에 단순화를 요청하기 전에 이 결과의 변화를 알아챌 방법이 필요합니다.
label:detail 문자열로 날짜가 있는 메모를 쓰는 포매터를 가정합니다. 특성화 테스트 예제를 Node.js 22 이상으로 실행하세요. 기록된 실행은 외부 의존성 없이 22.22.1을 사용했습니다.
더 나은 구현을 제안하기 전에 입력 몇 개 관찰
압축을 풀고 characterization-tests-before-ai-refactoring에서 실행하세요.
node observe.mjs original
입력, 주어진 시각, 환경 및 출력을 포함한 관찰 12개를 JSON으로 출력합니다. 일반 문자열에서 시작해 하나씩 바꾸세요. 그중 입력과 출력만 표시한 두 항목입니다.
[
{
"text": "build:cache:hit",
"output": "2026-10-13 | BUILD | cache"
},
{
"text": " build : ready ",
"output": "2026-10-13 | BUILD | ready"
}
]
두 번째 출력은 마지막 | 앞뒤에 공백이 두 개입니다. 보이는 상태로 조사하세요. 다른 관찰에서는 build와 build:가 모두 -로 끝납니다. 빈 라벨은 주어진 NOTE_LABEL, 그 값이 없거나 비면 note를 사용합니다.
original.mjs를 여세요.
export function formatNote(text, runtime = systemRuntime) {
const fields = text.trim().split(':');
const fallback = runtime.readEnv('NOTE_LABEL') || 'note';
const label = (fields[0] || fallback).toUpperCase();
const detail = fields[1] || '-';
const day = runtime.now().toISOString().slice(0, 10);
return `${day} | ${label} | ${detail}`;
}
fields[1]이 hit가 사라진 이유입니다. 이후 필드는 사용하지 않습니다. 첫 trim()은 입력 전체에 적용되어 build 뒤와 ready 앞 공백을 남기고 렌더링이 구분자 공백을 더합니다.
관찰은 수정 선택 전에 조사할 대상을 줍니다. Michael Feathers의 Unit Conversations도 기존 코드 호출, 응답 확인 및 유용한 상호 작용을 테스트로 저장하는 과정을 설명합니다.
날짜와 환경을 명시적인 입력으로
오늘 날짜를 저장한 출력은 내일 실패합니다. 개발자의 NOTE_LABEL도 빈 라벨 결과를 바꿉니다. 둘 다 테스트 입력이어야 합니다.
예제는 선택적 runtime 인수를 교체 지점으로 제공합니다. 기본값은 시계와 지정된 프로세스 환경 변수를 읽지만 모든 문서화된 관찰과 검사는 runtime.mjs의 다음 구현을 전달합니다.
export function fixedRuntime({ at, env }) {
return {
now: () => new Date(at),
readEnv: (name) => env[name],
};
}
대부분은 2026-10-13T12:00:00.000Z와 { NOTE_LABEL: 'daily' }를 사용합니다. 2026-10-13T23:30:00-07:00 사례는 toISOString()이 UTC를 사용해 2026-10-14로 시작합니다. 환경 값 누락과 빈 값도 별도로 검사합니다.
이런 교체 지점은 흔히 seam이라 부릅니다. Fowler의 Legacy Seam은 의존성을 분리하여 테스트가 동작을 선택하게 하는 방법을 다룹니다. 예제는 이미 인수를 제공하지만 실제 코드에 의존성이 묻혀 있다면 파서 재구성 전에 최소한의 교체 지점을 별도 변경으로 도입하고 기본 경로의 값 획득 방식을 유지하세요.
전역 시계나 프로세스 환경을 바꾸는 테스트는 없습니다. 자식 프로세스에 TZ=Pacific/Kiritimati와 다른 NOTE_LABEL을 설정해도 통과하여 주입한 입력이 관찰을 결정함을 확인했습니다. 실제 앱과 기본 어댑터의 연동은 별도 통합 검사가 필요합니다.
의문스러운 동작도 그대로 기록
탐색 입력은 cases.mjs에 있습니다. 원래 결과를 실행·확인한 뒤 예상 밖 공백과 버린 필드를 포함한 정확한 문자열을 characterized.json에 저장했습니다.
특성화 테스트는 선택한 동작이 바뀌었는지 묻지 그 동작의 정확성을 증명하지 않습니다. 예를 들어 cache를 보존해도 실제 앱이 cache:hit를 유지해야 하는지는 미결입니다.
run.mjs는 저장된 JSON과 각 구현의 결과를 비교합니다.
for (const item of fixtures) {
test(item.name, () => {
const actual = versions[mode](item.text, fixedRuntime(item));
assert.equal(actual, item.output);
});
}
단언 중 원래 포매터를 호출해 예상값을 만들지 않으므로 원본을 바꿔도 저장된 답이 조용히 바뀌지 않습니다. observe.mjs도 출력만 하며 fixture를 덮어쓰지 않습니다.
원래 구현을 기록과 비교하세요.
node run.mjs original
12개가 모두 통과합니다. 일반 입력, 상세 누락·빈 값, 후속 필드, 내부 공백, 빈 입력, 대체 라벨 유무 및 UTC 날짜를 다룹니다. 선택된 문자열 입력뿐이며 Unicode 대소문자, 잘못된 날짜, 비문자열 인수, 부수 효과 순서 및 실제 호출자는 포함하지 않습니다.
저장된 결과를 유지하며 리팩터링
동작 보존 버전은 파싱과 렌더링을 이름 있는 함수로 추출하며 원래 필드 선택을 유지합니다.
function parseFields(text) {
const [label, detail] = text.trim().split(':');
return { label, detail };
}
function render(day, label, detail) {
return `${day} | ${label} | ${detail}`;
}
refactored.mjs는 대체 규칙, 대문자 변환 및 UTC 날짜를 유지합니다.
node run.mjs refactored
12개 관찰이 모두 통과하고 전체 출력도 원본과 바이트 단위로 같습니다. 기록된 입력에 대한 지지 근거이며 미검사 사례는 남아 있습니다.
AI에 도움을 요청한다면 소스와 특성화 사례를 주고 기존 문자열을 보존하며 동작을 바꿀 제안을 구분하도록 하세요. 단순히 ‘정리해 줘’라고 하면 공백이나 구분자를 임의로 결정할 여지가 큽니다.
그럴듯한 정리가 실패하게 두기
accidental.mjs의 가상 정리는 편해 보이는 파서를 사용합니다.
function parseFields(text) {
const [label, ...rest] = text
.trim()
.split(':')
.map((field) => field.trim());
return { label, detail: rest.join(':') };
}
각 필드를 다듬고 나머지를 다시 합칩니다. 세 버전을 따로 실행하세요.
node run.mjs original
node run.mjs refactored
node run.mjs accidental
| 버전 | 통과 | 실패 | 종료 코드 |
|---|---|---|---|
original |
12 | 0 | 0 |
refactored |
12 | 0 | 0 |
accidental |
10 | 2 | 1, 예상된 실패 |
실패는 앞의 의외인 두 사례입니다. build:cache:hit는 이제 cache:hit로 끝나고 공백 입력은 2026-10-13 | BUILD | ready가 됩니다. fixture를 재생성해 변화를 숨기지 않았습니다.
node observe.mjs accidental로 새 출력을 보세요. 예상 실패 뒤 계속하려면 따로 실행하세요. &&로 연결하면 코드 1에서 멈춥니다. 기본 node --test run.mjs는 original만 검사합니다.
빨간 결과는 기록된 동작이 바뀌었다는 뜻입니다. 새 출력이 더 나은지는 다음 결정입니다.
동작 변경 하나를 별도로 결정
DECISION.md는 추가 콜론에 대해 cache 대신 cache:hit를 보존하는 제안과 전후 출력을 기록하며 동작 보존 리팩터링에서는 제외합니다.
실제 앱에서는 대표 입력과 문자열 사용처를 확인한 뒤 채택하세요. 저장된 관찰만으로 사용자가 버린 부분을 기대하는지 짧은 형태에 의존하는지 알 수 없습니다. 새 규칙을 승인하면 별도 패치로 구현하고 이유와 함께 해당 예상값을 갱신하며 새 합의 동작의 사례를 추가하세요. 공백과 날짜 규칙은 그대로 두세요.
여기서 코드 변경 영향 분석이 유용합니다. 관찰은 차이를, 호출자와 테스트는 결과를 평가하게 합니다. 테스트도 포함된 패치는 함께 바뀐 단언문 검토로 여전히 의도한 동작을 검사하는지 확인하세요.
관찰로 다음 읽기 방향 결정
값의 출처를 모르면 낯선 코드베이스에서 값 하나 따라가기부터 시작하세요. 포매터를 찾으면 현재 구현, 기록된 사례와 리팩터링을 함께 두세요.
다음처럼 AI에 요청할 수 있습니다.
포매터가 기록된 각 출력을 만드는 과정을 설명해 주세요. 버린 필드, 유지한 공백, 대체 라벨과 날짜의 코드를 짚어주세요. 관찰을 보존하는 리팩터링과 동작 변경 제안을 분리하고 저장된 출력을 요구 사항으로 추정하지 마세요.