It Was Just a Patch Update. What Could Possibly Go Wrong?
This is a submission for DEV's Summer Bug Smash: Smash Stories powered by Sentry. You know those...
개요 #
버전 번호의 맨 끝자리, 그러니까 패치 버전만 올렸을 뿐이다. 작은 버그 수정이나 들어 있을 그 숫자 하나. 그런데 그것이 애플리케이션 전체를 망가뜨렸다. 프레임워크의 잘못은 아니었다. 문제는 우리 코드에 있었다.
Sylwia Laskowska가 10년도 더 지난 2017년의 사건을 다시 꺼낸 이유는 단순한 추억팔이가 아니다. 그 시절 프론트엔드에서 저질렀던 실수가, AI 에이전트가 빠르게 자라나는 지금 오히려 더 위험한 형태로 반복되고 있기 때문이다.
Photo by Markus Winkler on Pexels
다시 2017년으로 #
먼저 버전 번호 얘기부터. package@x.y.z에서 x는 호환성이 깨지는 변경을 담는 메이저, y는 하위 호환을 지키는 기능 릴리스, z는 대개 작은 수정과 버그픽스가 들어가는 패치다. 프로젝트를 건강하게 유지하려면 최소한 패치 버전은 꾸준히 올려야 하고, 요즘 패키지 매니저는 이 작업을 알아서 해주기도 한다.
당시는 웹 애플리케이션이 세상을 막 집어삼키기 시작하던 무렵이었다. ES6가 나온 지 2년쯤 됐지만 브라우저 지원이 시원치 않아서 Promise나 화살표 함수, let과 const조차 조심스럽게 쓰던 시기다. 개발자 수요가 워낙 폭발적이라 export class 한 줄 쓸 줄 알면 어디든 취업이 되던 시절이었고, 밋업마다 "Angular 입문" 같은 발표가 넘쳐났다. 새 Angular가 세상에 나온 지 1년도 안 됐을 때다.
글쓴이는 Angular를 거의 초창기부터 써온 팀에서 야심 찬 미들 개발자로 일하고 있었다. 담당 프로젝트는 공정무역 인증 제품을 모니터링하는 대형 엔터프라이즈 애플리케이션. 서유럽부터 몇 주에 한 번 공공 도서관의 느린 회선으로 접속하는 아프리카 소국까지, 전 세계 어디서나 돌아가야 했다. 미래를 견디는 설계가 중요할 수밖에 없었다.
팀은 이 앱을 만들면서 동시에 현대 프론트엔드를 배워나갔다. Observable은 어떻게 동작하는가, RxJS는 정확히 뭔가, 서비스는 언제 써야 하나. Angular 자체도 아직 여물지 않아서 버그 때문에 기능이 그냥 안 되는 일이 잦았다. 이슈를 올리고 또 올렸는데, Angular 팀의 대응 속도는 놀라웠다. 이슈 템플릿을 다 채우기도 전에 고쳐놓는 경우까지 있었다. 그래서 마이너든 패치든 계속 올렸다.
초창기 Angular i18n의 한계 #
앞서 말했듯 이 앱은 여러 언어를 지원해야 했다. Angular에도 i18n의 초기 형태가 있긴 했지만 지금과는 딴판이었다. 번역 대상 요소를 i18n 속성으로 표시하는 방식이다.
<h1 i18n>Hello</h1>Angular CLI가 앱을 훑어 이 요소들로 번역 파일을 만들어냈다. 문제는 언어마다 별도의 빌드가 필요하다는 점이었다. 런타임에 언어를 바꾸는 건 사실상 불가능했다. 그런데 이 앱에는 런타임 언어 전환이 필수 요구사항이었다(정작 글쓴이는 이제 그 이유가 뭐였는지 기억나지 않는다고 한다).
당시 Angular 생태계는 너무 어렸고 이 문제를 풀어줄 성숙한 라이브러리가 없었다. 그래서 직접 만들기로 했다. 구현은 의외로 단순했다. 언어가 바뀔 때마다 페이지에서 i18n 속성을 가진 모든 요소를 찾아 내용을 알맞은 번역으로 교체하는 것. 열댓 줄이면 되는, 우아하고 단순한 코드였다.
패치 하나로 무너지다 #
모든 게 잘 돌아갔다. 2017년 중반까지는.
Angular는 어느새 버전 4에 와 있었다(그럴 만한 이유로 3을 건너뛰었는데, 그 이유가 뭐였는지도 이제는 기억나지 않는다고). 어느 날 지극히 평범한 업그레이드를 했다. 정확한 숫자는 흐릿하지만 대략 4.2.4에서 4.2.8 정도. 아주 작은 패치였고, 아무 일도 없어야 했다.
그런데 번역이, 국제화 시스템 전체가 통째로 사라졌다. 앱은 멀쩡히 뜨고 화면도 정상이었다. 하지만 언어를 바꾸려 하면 아무 반응이 없었다. 영어에 갇혀버린 것이다.
Photo by Miguel Á. Padriñán on Pexels
디지털 수사에 나서다 #
여기서부터가 글쓴이가 가장 좋아하는 부분, 소프트웨어 포렌식이다. 처음엔 그 작은 Angular 업데이트가 범인일 리 없다고 생각했다. 패치야 늘 올리는 거니까. 당시엔 CI 파이프라인이나 자동화 테스트도 걸음마 단계였다. 테스터가 한두 주 언어를 안 바꿔본 것 아닐까 하는 의심부터 들었다.
Git 히스토리를 뒤졌지만 국제화를 건드린 커밋은 없었다. 백엔드가 깨졌나, 번역 파일이 사라졌나 확인했지만 전부 제자리였다. 결국 내키지 않는 마음으로 Angular 업그레이드를 다시 들여다봤다. 패치를 하나씩 되돌려가며 확인하니, 어느 두 패치 버전 사이에서 정확히 번역이 멈췄다. 그렇다면 범인은 Angular인가. 아니, 정말 그런가.
미스터리의 해답 #
Angular 체인지로그를 파고들다 이런 변경을 찾아냈다.
컴파일러가 할 일을 다 끝냈는데 굳이
i18n속성을 생성된 HTML에 남겨둘 이유가 있나? 지우자.
모든 게 설명됐다. 우리의 런타임 번역 시스템은 Angular가 한 번도 보장한 적 없는 구현 세부사항에 통째로 기대고 있었던 것이다. Angular 입장에서 그 속성을 지우는 건 지극히 합당한 정리였다. 하지만 우리에겐 앱 전체가 무너지는 사건이었다.
참고로 이 변경은 Angular 4.2.6에 들어간 "compiler: remove i18n markup even if no translations"였다. 관련 PR #17999, 원래 이슈 #11042, 후속 논의 #20055에서 확인할 수 있고, 피해자가 우리만은 아니었다.
그래서 우리가 형편없는 엔지니어였나? #
수정 자체는 어렵지 않았다. 그저 귀찮았을 뿐. Angular 내부의 i18n 속성에 기대는 대신 자체 디렉티브를 만들었다. 이름은 fi18n. 창의력의 정점이었다며 글쓴이는 웃는다.
이 이야기가 우리가 뭘 하는지도 모르는 초보였다는 뜻일까? 오히려 정반대다. "런타임 언어 전환이 멋져지기도 전에" 그걸 구현해낸 팀이었다. 유일한 실수는 우연히 관찰한 동작을 Angular의 공개 계약이라고 넘겨짚은 것뿐이다. 다행히 앱이 아직 프로덕션 근처도 아니었던 덕에, 젊은 날의 낙관으로 무사히 넘어갔다.
왜 하필 이 버그인가 #
10년 넘게 개발하며 겪은 훨씬 큰 프로덕션 사고들을 두고 왜 이 버그를 꺼냈을까. 교훈이 지금 더 절실해졌기 때문이다.
2026년의 프론트엔드는 완전히 달라졌다. 이제 아무도 "Angular 입문" 발표를 하지 않는다. React, Angular, Vue 모두 성숙한 생태계고, 어떤 문제든 누군가 이미 풀어놨을 가능성이 높다. 게다가 AI가 웬만한 반복 작업은 대신 해준다.
그럼 이런 사고가 오늘날엔 안 일어날까. 천만에. 다만 이제 무대가 프론트엔드가 아닐 뿐이다. 지금은 AI, 특히 AI 에이전트라는 새 생태계가 무서운 속도로 자라고 있다. 컨퍼런스와 밋업이 끊임없이 열리는 건 아직 아무도 정답을 모르기 때문이다. 에이전트 루프가 어떻게 도는지 설명하는 글이 여전히 쓰이고 있고, MCP 같은 프로토콜은 따라잡으려면 진짜 노력이 필요할 만큼 빠르게 바뀐다. 2017년의 프론트엔드와 꼭 닮았다.
그때처럼 개발 중 위험한 가정을 하기도 쉽다. 누군가는 구조화된 출력 대신 정규식으로 모델의 자유 형식 응답을 파싱한다. 누군가는 모델이 항상 똑같은 마크다운 블록 안에 JSON을 감싸줄 거라 믿는다. 누군가는 특정 프로바이더가 반환하는 문서에 없는 필드로 비즈니스 로직을 짠다. 누군가는 같은 프롬프트가 항상 같은 툴을 부를 거라 가정한다.
오늘도 되고 내일도 아마 될 것이다. 그러다 다음 달, 작은 업데이트가 사소해 보이는 무언가 하나를 바꾸면 전부 무너진다. 그 옛날 i18n 속성이 그랬던 것처럼.
진짜 교훈 #
돌아보면 이 이야기는 Angular에 관한 게 아니다. 훨씬 보편적인 무언가에 관한 것이다. 엔지니어는 관찰된 동작(observable behavior)을 보장된 계약(guaranteed contract)으로 착각하곤 한다. 오늘 존재한다고 해서, 만든 사람이 당신에게 그걸 믿고 쓰라고 의도한 건 아니다. 문서화되지 않은 동작에 기댄 해결책은 단단한 땅 위에 서 있는 게 아니라, 그저 지금까지 운이 좋았을 뿐이다.
그렇다고 실수를 자책하라는 얘기는 아니다. 누구도 처음부터 다 맞히지 못한다. 중요한 건 거기서 배우는 것이다. 이 프로젝트는 해피엔딩으로 끝났다. 글쓴이는 오래전 떠났지만 앱은 지금도 잘 살아 있고, 이 글을 쓰기 전 확인해보니 거의 10년 전 자신이 디자인한 로그인 화면이 그대로 남아 있었다고 한다. 그게 그렇게 뿌듯했다며 글은 끝을 맺는다.
이 글은 위 출처를 바탕으로 한국 독자를 위해 재작성한 기사입니다. 원문의 사실과 수치에 근거하며, 별도의 견해를 포함하지 않습니다.

