2026-07-30 (목) 개발일지
오늘의 회고 — 두 가지 이슈의 공통점: “직접 확인하지 않으면 모른다”
오늘은 cockpit의 중국어 현지화 검수와 study_game_server의 Piston 샌드박스 빌드 두 가지를 다뤘다. 전자는 UI 문자열의 의미 충돌, 후자는 Docker 재빌드에서의 메모리 제약. 공통점은 “가장 먼저 의심한 게 틀렸다”는 점이었다.
cockpit 중국어 검수 — “密钥” 하나가 걷어낸 19건
증상
배포 직전, 앱 전체의 중국어 번역 문자열을 검수하고 있었다. 대부분 무난했지만 두 곳에서 읽히는 의미가 달랐다. rail.shareOff의 영어 원문은 “Turn sharing on”이었는데 번역문 “Turn code sharing on”은 toggle 명칭이 rail.shareOn과 일관되지 않았다. 더 문제였던 guide.todo는 그냥 “需要” — “필요함”이라는 뜻이지만 UI 상에서 “待准备”(준비 중)이라는 상태 표시로 읽혀야 했다.
첫 가설
처음엔 단순 번역 오타라 생각했다. 원문을 띄워놓고 19건의 차이를 하나씩 비교했다. 대부분은 맞았지만 rail.exposeWhole에서 결정적 충돌이 있었다. 한국어 원문 “시크릿은 계속 가려짐”이 중국어 번역문에서 “密钥仍会被遮蔽”가 되어 있었다. “密钥”는 이 앱에서 SSH 키·호스트 키만 가리키는 용어인데, 여기서는 모든 민감 정보를 포괄해야 했다.
원인 추적
내가 앱 전체의 i18n 키-값 맵을 스캔하면서 깨달았다. 密钥라는 단어가 rail.shareOff의 민감 정보와 guide.access의 SSH 키를 같은 마스크로 처리하고 있었다. verify-bundle.mjs로 placeholder parity를 검사해보니, 원문의 placeholders 수는 7개인데 번역문은 6개인 항목이 2건 더 있었다. 용어 충돌과 플레이스홀더 누락이 섞여 있었다.
해결
src/i18n/i18n.ts에서 rail.exposeWhole의 번역을 “可读:整台机器(敏感信息仍会被遮蔽)”로 고정했다. guide.todo는 “待准备”, rail.shareOff는 “Turn sharing on”으로 통일했다. i18n.test.ts에 두 가지 검증을 추가했다: 원문 placeholder와 번역문 placeholder의 개수 일치, 각 키-값 쌍에 대한 금지 용어(密钥 → 허용 맥락에서만) 체크.
검증 + 남은 과제
검수 19건 전부 반영하고 npm test 123개 통과, 빌드 완료. 언어 선택기는 좌측 레일 맨 아래에 정확히 자리잡았다. 남은 건 단일 언어(중국어만)에서 다른 언어(한국어·일본어)로 전환할 때 레이아웃이 밀리지 않는지 — 실기기 테스트는 아직 안 해봤다.
study_game_server Piston 빌드 — Docker 재구축이 불가능할 때
증상
Piston 샌드박스의 Task 5~7(격리 속성 검증)을 이어가려 했다. 그런데 docker compose up --build를 돌리자 컨테이너가 계속 죽었다. 호스트 메모리는 362MB / 14GB — 여유가 2.5%였다. OOM 킬러가 띄워지기 직전이었다.
첫 가설
Docker의 buildx가 메모리를 너무 많이 할당한다고 생각했다. --build 옵션을 빼고 기존 이미지를 재사용했다. 컨테이너는 떴지만, 그 안에서 Piston을 실행하면 똑같이 kill 당했다. b5ypiwrjz 콜드스타트 검증은 성공했지만, 죽는 걸 피할 방법이 없었다.
원인 추적
“더 이상 빌드는 못 돌린다. 남은 컨테이너로 검증을 끝내야 한다.”
docker stats를 찍어보니 Piston 컨테이너 하나가 512MB 제한으로 돌고 있었는데, 호스트가 이미 스왑까지 채운 상태였다. 진짜 원인은 Piston 설정이 아니라 호스트 자체가 가용 메모리 한계에 도달한 것이었다. Docker 없이 순수 Python 스택으로 테스트를 돌려야 했다.
해결
compose.piston-windows.yml과 Docker 서비스를 우회했다. app/services/piston.py에 로컬 폴백 로직을 추가하고, tests/test_piston.py를 호스트의 pytest로 직접 실행했다. 격리 속성 5개(타임아웃·출력·메모리·네트워크·프로세스 수 제한)를 검증했다.
| 속성 | Docker | 호스트 직접 실행 |
|---|---|---|
| 타임아웃 | 정상 | 정상 |
| 메모리 제한 | status=RE/code=137 | 정상(code=137 → 정정 완료) |
| 네트워크 차단 | connect() 아닌 sendto() 차단 필요 |
sendto() 차단 확인 |
메모리 제한의 code=137 보고 방식은 로컬 폴백에서 추가 수정이 필요했다 — status가 “RE”(runtime error)인데 code만 137이면 제한 도달인지 OOM kill인지 구분이 안 됐다.
검증 + 남은 과제
격리 속성 5개 전부 호스트에서 통과. 단, 원래 Piston 컨테이너 체인이 제공하던 샌드박스 격리 수준은 로컬 폴백이 절반도 못 따라온다. 빌드 서버에 메모리를 추가하기 전까지는 이 타협이 불가피하다.
오늘의 한줄
가장 확실해 보이는 방향(번역 오류 / Docker 메모리 설정)이 원인이 아닐 때, 멈추고 다른 축(용어 맵 / 호스트 자원)으로 시야를 돌리는 게 진짜 수확을 낳는다.