Skip to content
게으른 엔지니어의 기술 블로그
Go back

Hermes에게 lint 정리를 맡겼다가 2편 — YAML 날짜가 조용히 사라진 이유

지난 글에서 Hermes에게 맡긴 542건의 타입 안전성 문제는 다 정리했다. 남은 건 262건이었는데, 이번엔 타입 안전성이랑은 상관없는 자잘한 것들이었다. 안 쓰는 변수, 이상한 정규식, Obsidian 플러그인 규칙 위반, Promise를 제대로 안 기다리는 문제 같은 것들. 네 묶음으로 나눠서 순서대로 맡겼다.

”아직 검증 안 했다”는 정직한 보고

두 번째 묶음을 작업하던 중 이런 보고가 왔다.

Remaining G2 work: Verify the tag-utils.ts ! assertions satisfy the linter, run full lint/build/tests, commit.

“아직 전체 테스트를 안 돌려봤다”는 얘기였다. 그래서 이번엔 보고를 더 기다리지 않고 내가 먼저 직접 테스트를 돌려봤다. 이 프로젝트엔 테스트 파일이 30개 있는데, 그걸 전부 하나씩 실행하는 명령이다.

for f in tests/*.mjs; do node "$f" > /tmp/t.out 2>&1 || echo "FAILED: $f"; done
FAILED: tests/note-exporter-spec.mjs
FAILED: tests/status-workflow-spec.mjs

두 개가 실패하고 있었다.

하나는 원인이 바로 보였다

note-exporter-spec.mjs 쪽은 에러 메시지만 봐도 알 수 있었다.

TypeError: (0 , _obsidian.requestUrl) is not a function

이번 lint 정리 규칙 중에 “fetch 대신 Obsidian이 제공하는 requestUrl을 써라”는 게 있는데, 그걸 적용하면서 코드를 바꿨다. 문제는 테스트용 가짜 Obsidian 환경(mock)에는 requestUrl이라는 함수가 아직 만들어져 있지 않았던 거다. 실제 Obsidian 안에서는 문제없이 동작하겠지만, 테스트를 돌릴 땐 그 가짜 함수가 없으니 그냥 에러가 났다. 이건 가짜 함수를 하나 채워 넣으면 끝나는, 비교적 단순한 문제였다.

다른 하나는 원인을 찾기 까다로웠다

status-workflow-spec.mjs는 좀 이상했다. 이 테스트가 확인하는 파일 중에 직접 손댄 건 없어 보이는데 실패하고 있었다. 그래서 git stash(코드 변경 사항을 잠깐 치워뒀다가 나중에 다시 꺼내 쓸 수 있는 기능)를 이용해서, 의심 가는 파일을 하나씩 원래 상태로 되돌려가며 어느 파일 때문인지 좁혀나갔다.

git stash push -- src/utils/wiki-utils.ts
node tests/status-workflow-spec.mjs 2>&1 | grep -A3 "T-STATUS-2"
# Result  : PASS ✅

wiki-utils.ts 파일 하나만 원래대로 되돌리니 테스트가 다시 통과했다. 범인을 찾은 거다. 이 파일에서 뭐가 바뀌었는지 봤다.

// 원래 코드
if (!created) created = String(parsed.created || "");

// Hermes가 no-base-to-string 규칙 때문에 바꾼 코드
const fmStr = (val: unknown): string => typeof val === 'string' ? val : '';
if (!created) created = fmStr(parsed.created);

no-base-to-string은 “이 값이 항상 문자열이라는 보장이 없는데 String()으로 억지로 문자열 취급하고 있다”는 걸 잡아내는 규칙이다. 지적 자체는 맞는 말이라, 고친 방식도 그럴듯해 보였다 — 진짜 문자열이면 그대로 쓰고, 아니면 빈 문자열로 처리하겠다는 것.

그런데 이 값은 Obsidian 노트 파일 맨 위에 있는 YAML이라는 형식의 메타데이터에서 가져오는 거였고, 테스트 데이터엔 이렇게 쓰여 있었다.

created: 2026-06-20

따옴표 없이 그냥 쓴 날짜다. 그런데 YAML은 이렇게 따옴표 없는 날짜를 만나면, 이걸 문자열이 아니라 날짜(Date) 형태의 값으로 자동 변환해버린다. 겉보기엔 그냥 텍스트 같지만, 실제로 코드가 받는 값은 문자열이 아니라 날짜 객체였던 거다. 그러니 “이 값이 문자열인가?”를 확인하는 fmStr 함수는 이걸 문자열이 아니라고 판단했고, 그냥 빈 문자열로 바꿔서 날려버렸다. 원래 있던 String(val || "") 코드는 뭐가 오든 무조건 문자열로 바꿔버리는 방식이라 이런 문제가 없었는데, “더 안전해 보이는” 새 코드가 오히려 값을 조용히 지워버린 거다.

남는 교훈

no-base-to-string을 고친다는 건 “이 값이 항상 문자열이라고 확신할 수 없다”는 경고에 대응하는 일이었다. 그런데 정작 “그 값이 문자열이 아닐 때 어떻게 해야 하는가”는 규칙이 알려주지 않는다. 문자열로 변환해야 하는지, 그냥 버려도 되는지는 그 값이 실제로 어디서 오는지 아는 사람만 판단할 수 있다. 이번 경우엔 값이 YAML에서 온다는 걸 감안해서 날짜나 배열 같은 타입까지 제대로 문자열로 바꿔줬어야 했는데, 제일 손쉬운 방법(문자열이 아니면 그냥 버린다)을 택하면서 값이 사라져버렸다.

lint 도구는 “여기 위험할 수 있다”까지만 알려준다. 그 위험을 실제로 어떻게 없앨지는 코드만 봐서는 안 되고 그 값이 어떤 데이터인지 알아야 판단할 수 있는 부분이라, 판단이 틀리면 lint는 통과하는데 실제 동작은 깨지는 일이 생긴다. 겉보기엔 그냥 lint 경고 하나 없앤 것뿐이었지만, 실제로는 값 하나가 조용히 사라지는 회귀였다 — 테스트가 없었다면 다음 배포 때까지 아무도 몰랐을 거다.

이 두 문제는 원인까지 정확히 짚어서 다시 Hermes에게 전달했고, 다음 라운드에서 둘 다 제대로 고쳐졌다. 그 이야기는 다음 편에서.


Share this post:

Previous Part
Hermes에게 lint 정리를 맡겼다가 — 보고도, 내 계산도 둘 다 다시 확인해야 했다
Next Part
Hermes에게 lint 정리를 맡겼다가 3편 — AI가 스스로 멈춘 순간