번역 공장 하루, 그 사이 기능도 심었다
목차
오전을 통째로 번역 배치에 쓰고 나서 느낀 건, 숫자가 클수록 진척감이 오히려 줄어든다는 거다. 멤버 바이오 688개, 그룹 요약 137개, 신규 칼럼 24개, 전부 8개국어. 산술로 따지면 오늘 처리한 셀만 수천 개인데, 완료 체크를 치면서도 "이제 시작이네" 싶은 감각이 드는 게 이상하면서도 익숙하다. 바이오 기준으로 50/688에서 출발했으니 7% 남짓 온 거고, 아직 갈 길이 한참 남았다.
수치만 보면 이렇다.
| 항목 | 건수 | 언어 수 | 셀 규모(추정) |
|---|---|---|---|
| 멤버 바이오 | 688 | 8 | ~5,500 |
| 그룹 요약 | 137 | 8 | ~1,100 |
| 신규 칼럼 | 24 | 8 | ~190 |
다국어 DB를 이 규모로 운영하는 이유 자체는 분명하다. 검색 트래픽의 상당 부분이 비영어권에서 오고, 모국어로 검색한 사용자가 영어 콘텐츠로 이탈하면 그게 숫자로 직결된다. 8개 언어라는 범위도 욕심이 아니라 실측치에서 나온 거다. 다만 배치가 완료되기 전까지 빈 셀이 인덱싱될 수 있어서, 속도보다 정확도 관리가 더 중요하다. 잘못된 번역이 들어간 채로 인덱싱되면 수정 배치가 하나 더 필요해지니까.
번역 배치 작업 특유의 피로가 있다. 단어를 고르거나 문장을 다듬는 게 아니라, 파이프라인 돌리고, 결과 검수하고, 예외 케이스 골라내는 루프의 반복. 한 배치가 끝나면 다음 배치가 대기 중이고, 문서에 "완료"를 적는 순간에도 다음 줄이 눈에 들어온다. 감각 마비가 딱 맞는 표현이다.
컴백 Web Push, 손이 가는 작업
번역 루프에서 잠깐 빠져나와서 한 게 컴백 Web Push 기능이었는데, 여기서 에너지가 좀 회복됐다. 서비스 워커 등록, 구독 토큰 저장, 발송기 연결까지 흐름을 다 이었을 때 체감이 달랐다. "실제로 사람 화면에 뜨는 기능"이라는 게 만들면서 느껴지는 거, 배치 작업이랑 분명히 다르다.
Web Push 자체는 구조가 크게 복잡하지 않다. 브라우저가 VAPID 키로 서버를 검증하고, 구독 객체(endpoint + keys)를 저장해두면, 서버에서 암호화해서 쏘는 방식.
// 서비스 워커 등록 + 구독 흐름
const reg = await navigator.serviceWorker.register('/sw.js');
const sub = await reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY),
});
await fetch('/api/push/subscribe', {
method: 'POST',
body: JSON.stringify(sub),
});
토큰 저장 로직에서 신경 쓴 건 중복 구독 처리였다. 같은 기기에서 재구독하면 endpoint가 갱신될 수 있고, 이걸 upsert 방식으로 처리하지 않으면 발송 실패가 조용히 쌓인다. 발송기 쪽에서도 410 Gone 응답을 받으면 토큰을 즉시 삭제하는 로직을 같이 넣었다. 안 넣으면 죽은 토큰이 계속 누적되고, 나중에 발송 실패율이 조용히 올라간다.
기념일 카운트업이나 공유 버튼도 같은 맥락으로 붙였다. 사용자가 직접 누르거나 볼 수 있는 기능은 "이게 왜 있어야 하는가"가 명확하니까 손이 더 잘 간다. 번역 배치랑 병행하면서 이 차이를 더 크게 느꼈다.
대시보드 정비, Date 버그 하나
오후는 대시보드 쪽을 건드렸는데, 솔직히 에너지가 바닥에 가까웠다. KPI 그리드 열 정렬, 사이트 헬스 49개 항목 전부 노출, 미분류 25개 카테고리 분류. 하나하나는 10분짜리인데 계속 "이것도 안 됨, 저것도 안 됨"이 나오니까 패턴이 됐다. 에너지가 조금씩 빠지는 게 느껴졌다.
그나마 건진 건 Date 강제 변환 처리였다. 특정 경로에서 날짜 필드가 문자열로 내려오는 경우가 있었는데, 그 상태로 날짜 연산을 돌리면 NaN이 나오거나 런타임에서 터진다.
// 수정 전: 문자열이 들어오면 NaN 또는 런타임 에러
const diff = Date.now() - record.createdAt; // string이면 뻥
// 수정 후
const ts =
typeof record.createdAt === 'string'
? new Date(record.createdAt).getTime()
: record.createdAt;
const diff = Date.now() - ts;
이런 버그는 개발 환경에서 거의 안 보인다. 시드 데이터는 타입이 제대로 붙어 있고, 실제 운영 데이터에서만 문자열로 오는 경우가 생긴다. 잠재적인 500이라는 표현이 맞다. 어떤 조건에서 어느 사용자한테 터질지 예측이 안 되는 거, 놔뒀으면 뒤늦게 로그 보면서 원인 추적하는 시간을 썼을 거다.
로스터 전수 점검은 그나마 집중이 됐다. 실제 DB를 들여다보면서 결측치나 이상값 찾는 작업, 배치 피로가 있는 상태에서도 잠깐 긴장감이 생기는 타입이라 버텼다.
AdSense 계정 두 번째 추가는 성장하는 느낌이었다. ads.txt 업데이트하고 추적 범위 넓히는 건 단순 작업이지만, 이게 심어지는 것 자체가 확장이다. 승인 검토 중이라 결과는 모르지만, 파이프라인 안에 들어왔다는 것만으로 기분이 나쁘지 않았다.
블로그 자동화 조정은 사소해 보여도 나름 중요했다. em-dash 금지 룰을 프롬프트에 박은 건 완전히 내 실수에서 나온 거다. 생성물 보다가 AI 티가 팍팍 나서 뭔가 싶었는데, 뜯어보니 em-dash가 거의 모든 문장에 들어가 있었다. LLM 출력에서 em-dash가 자주 나오는 건 학습 데이터 패턴 문제고, 명시적으로 금지하지 않으면 계속 나온다. 바로 프롬프트에 추가했다. 회고 발행 텀을 3시간에서 6시간으로 늘린 것도 비슷한 이유에서 나왔다. 너무 자주 나가면 오히려 밀도가 없어 보인다는 판단, SEO 관점에서도 글 하나하나의 무게가 분산되는 거라 간격을 늘리는 게 맞다고 봤다.
오늘 느낀 건, 산발적으로 여러 곳 건드리는 날은 끝나도 완성감이 없다는 거다. 각 작업이 다 의미는 있었지만, 맥락 전환 비용이 생각보다 크다. 내일은 멤버 바이오 번역에 오전을 몰아넣고, GSC 색인 핑 실제 동작 확인 정도만 추가로 보는 게 맞을 것 같다.
댓글 0
첫 댓글 달아줘.