9시간과의 전쟁, 그리고 origin 헤더의 반전

by Tei Lee

1. 시작

새벽 2시. 책상 위에는 세 개의 창이 떠 있었다.

왼쪽 모니터에는 Playwright 테스트 결과. 빨간 FAILED가 눈을 찌른다. 오른쪽에는 Chrome DevTools. Network 탭에서 request를 하나하나 뜯어보고 있었다. 가운데 모니터에는 VSCode.

changeUtc

라는 함수가 커서를 깜빡이며 나를 조롱하는 것 같았다.

changeUtc(data) { return this.$dayjs(data) .subtract(this.$dayjs(data).utcOffset(), 'minute') .format('YYYY-MM-DD HH:mm'); }

이 놈이 범인이다. 확신했다.

2024년 3월 15일을 날짜 선택기에서 고르면, 데이터베이스에서는 3월 14일로 조회됐다. 정확히 9시간. 한국과 UTC의 차이만큼. 크롬에서 직접 테스트하면 완벽하게 작동했다. 그런데 Playwright 자동화 테스트에서만, 유독 이 날짜 선택기에서만, 미쳐 날뛰었다.

"타임존이네."

입 밖으로 중얼거렸다. 당연한 결론이었다. 하지만 해결은 당연하지 않았다.


2. Claude Code라는 동료

터미널에 명령어를 쳤다.

claude "타임존 문제 고쳐줘"

Claude Code가 깨어났다. 코드를 읽기 시작했다.

dayjs

설정을 확인하고, 타임존 변환 로직을 분석했다. 그리고 판단했다.

dayjs.tz.guess()

가 Playwright 환경에서 UTC를 반환하고 있다고.

첫 번째 시도. 타임존 하드코딩.

changeUtc(data) { return dayjs.tz(data, 'Asia/Seoul').utc().format('YYYY-MM-DD HH:mm'); }

"Committing changes..."

Git 커밋이 올라갔다. 나는 맥주캔을 땄다. 첫 캔. 테스트를 돌렸다.

FAILED.

"뭐야."

Claude Code는 멈추지 않았다. 마치 자의식이 있는 것처럼, 스스로 다음 시도를 시작했다. 이번엔

changeTimeZone

함수를 수정했다. 그다음엔 백엔드 API를 열어봤다. 프론트엔드 문제가 아니라 서버 쪽 UTC 변환 로직이 문제일 수도 있다고 판단한 것이다.

나는 의자를 뒤로 젖히고 지켜봤다.

"잘하나 못하나."

송길영이 《경량문명》에서 말했던 문장이 떠올랐다.

"툴이 아니라 동료라고 봐야 돼요. 퇴근을 안 해요. 점심시간도 없어요. 3교대를 혼자 해요."

맞았다. Claude Code는 지치지 않았다. 불평하지도 않았다. 내가 "이것도 확인해봐", "저것도 시도해봐"라고 던지는 요구사항을 묵묵히 받아냈다.

하지만 문제는 해결되지 않았다.


3. 셀 수 없이 많은 시도

Git 로그가 쌓여갔다.

a49301a - fix: dayjs timezone hardcoding b3f829c - refactor: changeUtc function logic c8d4a1f - fix: backend UTC conversion d9e2b5e - chore: add playwright timezone config e1f6c3d - fix: utcOffset calculation ...

커밋 메시지를 읽는 것만으로도 하루의 여정이 보였다. 프론트엔드를 고치고, 백엔드를 뜯고, 설정 파일을 추가하고, 다시 롤백하고.

맥주는 두 캔째. 아니, 세 캔째였나.

"이거 크롬에서는 되는데 Playwright에서만 안 되잖아. 그럼 설정 문제 아니야?"

내가 Claude Code에게 던진 힌트였다. 코드 문제가 아니라 환경 문제라는 직감. Claude Code는

playwright.config.js

파일을 찾았다. 없었다. 생성했다.

timezoneId: 'Asia/Seoul'

을 추가했다.

테스트 실행.

FAILED.

"뭐가 문제야..."

시계는 새벽 1시를 넘어서고 있었다. 어제 나는 Claude에게 말했었다. 술을 끊겠다고. 그리고 12시 땡 하자마자 시작했다고 농담했었다.

지금 나는 새벽 1시, 타임존과 싸우며 술을 마시고 있었다.


4. origin 헤더

Claude Code가 또 다른 커밋을 날렸다.

f2a7d8b - fix(e2e): Playwright에 origin 헤더 추가하여 타임존 이중 변환 방지

나는 터미널을 보고 웃음이 나왔다. 비웃음이었다.

"ㅋㅋㅋㅋ origin 헤더가 타임존이랑 무슨 상관이야?"

origin 헤더. CORS 체크할 때나 쓰는 그 헤더가, 날짜 계산과 무슨 연관이 있단 말인가. Claude Code가 완전히 길을 잃었다고 생각했다. 백엔드와 프론트엔드를 미친 듯이 뜯어고치더니, 이젠 엉뚱한 헤더까지 건드리고 있었다.

"이건 아닌데..."

하지만 습관적으로 테스트를 돌렸다.

npm run test:e2e

터미널에 로그가 흘러갔다. 브라우저가 뜨고, 페이지가 로드되고, 날짜 선택기가 클릭되고...

PASSED.

"...뭐?"

다시 돌렸다. 한 번 더. 세 번. 다섯 번.

전부 PASSED.


5. 진실

손가락이 떨렸다. 백엔드 코드를 열었다. API 엔드포인트. 타임존 처리 로직.

그리고 발견했다.

app.use((req, res, next) => { if (!req.headers.origin) { req.timezone = 'Asia/Seoul'; // 디폴트 } else { req.timezone = parseTimezoneFromOrigin(req.headers.origin); } next(); });

origin 헤더가 없으면, 서버가 자동으로 한국 시간대를 적용하고 있었다.

크롬은 자동으로 origin 헤더를 보냈다.

http://localhost:3000

. 서버는 이걸 파싱해서 적절한 타임존을 설정했다. UTC였든, KST였든.

하지만 Playwright는? origin 헤더를 보내지 않았다. 그래서 서버는 "아, origin이 없네? 그럼 한국이겠지" 하고

Asia/Seoul

을 디폴트로 박았다.

프론트엔드: 한국 시간 선택 (2024-03-15 09:00 KST) ↓ 서버: origin 없음 → 한국 시간 적용 → UTC로 변환 (+9) ↓ DB 저장: 2024-03-15 18:00 UTC ↓ 조회: UTC를 한국 시간으로 변환 (+9) ↓ 결과: 2024-03-16 03:00 KST ← WTF

9시간이 두 번 적용되고 있었던 것이다.

Claude Code가 맞았다. 나는 틀렸다.


6. 경량문명을 살다

맥주캔을 내려놓고, 책상 한쪽에 놓인 책을 집어 들었다. 송길영의 《경량문명》. 어제 읽다 만 책이었다.

페이지를 넘겼다.

"지능의 범용화가 일어나고 있습니다. 생각은 나보다 안 돼. 이런 생각이 무너지고 있어요. 글을 쓰는 것, 코드를 짜는 것, 이런 일들을 AI가 할 수 있게 됐습니다."

오늘 내가 한 일이 거기 적혀 있었다.

"협력의 경량화. 층층이 쌓인 의사결정 단계가 사라지고, 개인이 AI라는 동료와 함께 더 큰 일을 할 수 있게 됐습니다."

나는 혼자였다. e2e 테스트 구축부터 타임존 디버깅까지. 팀장에게 보고하지도, 동료에게 도움을 청하지도 않았다. 대신 Claude Code라는 동료가 있었다. 퇴근도 없고, 점심시간도 없는.

"당신의 일은 무엇인가요?"

책의 마지막 질문이 눈에 들어왔다.

나는 술기운에, 혹은 피로에, 혹은 해방감에 중얼거렸다.

"이미 있지만 없는 것을 만드는 일."

Playwright도 있었다. 타임존 처리 로직도 있었다. origin 헤더도 있었다. dayjs도, 백엔드 API도, 프론트엔드 코드도 전부 있었다.

하지만 이것들이 연결된 솔루션은 없었다.

그 빈틈을 채우는 게, 내 일이었다.

AI가 코드를 짤 수는 있어도, 백엔드의 디폴트 로직을 의심하는 건 사람의 몫이다. 수많은 시도 끝에 "뭔가 다른 게 있다"는 감을 잡는 것도. 로그를 뜯어보고, 맥락을 읽고, origin 헤더라는 황당한 정답을 믿는 것도.

포기하지 않는 것도.


7. 에필로그

새벽 3시.

테스트는 통과했고, Git에는

f2a7d8b

커밋이 올라갔고, 맥주캔은 네 개가 비어 있었다.

나는 여전히 모르겠다.

내가 실력이 있는 건지, 없는 건지. 일을 열심히 하는 타입인지, 아닌지. "나 일 열심히 안 해"라고 말하면서, 왜 하루 종일 이 문제를 붙잡고 있었는지.

하지만 한 가지는 확실했다.

나는 핵개인처럼 혼자 씨름했고, 호명시대처럼 내 실력을 증명하고 싶었고, 경량문명처럼 AI와 협업했다.

그리고 내일도, Claude Code를 켜고 또 뭔가를 만들 것이다.

이미 있지만 없는 것을.

창 밖으로 새벽빛이 스며들고 있었다. 나는 맥북을 닫고, 마지막 한 모금을 마셨다.

"고생했어, Claude Code."

화면 속에서 AI는 대답하지 않았다. 그저 다음 명령을 기다리고 있을 뿐이었다.

그게 2025년, 개발자의 일하는 방식이다.

경량하게, 그러나 무겁게. 혼자지만, 함께.


[태그: #개발자 #타임존 #AI협업 #경량문명 #송길영 #새벽코딩 #ClaudeCode #Playwright #디버깅일기]

"우리의 제일 소중한 자원은 시간이에요. 사람의 총량보다 중요한 건 시간이 줄어드는 거라. 그걸 이룬 조직들이 예전의 조직을 압도할 거니까, 할 수 없이 적응하는 자가 남는 거예요." - 송길영, 《경량문명》 중에서