Player Unknown’s Bug: 원인 모를 문제를 기록하면 성장할 수 있나요?

원인 모를 문제를 해결하고 ‘위너위너 치킨디너’ 하는 방법

Chanhee Lee — 이찬희 @hiddenest

복잡한 환경과 원인을 알 수 없는 문제

개발을 하다 보면 도저히 원인을 알 수 없는 문제를 만나고는 합니다. 특히 웹 프론트엔드는 조금 더 꼬여있어서, 서버의 상태 외에도 브라우저의 종류나 버전, Chrome Extension 등 수많은 외부 요인이 강결합되어 있습니다. 그렇기에 문제의 원인을 찾고 해결하기 위해서는 많은 힘을 들여야 합니다.
개발자는 문제 해결 과정에서 두 가지가 필요합니다. 문제 대응 과정에서 사람을 비난하지 않는 것, 근본 원인을 규명해 재발을 막는 방법을 찾고 실행하는 것. 흔히 ‘포스트모템(Postmortem)’이라고 부르는 것입니다.
하지만 문득 그런 생각이 들었습니다. 만약 문제의 원인을 모른다면, 혹은 문제의 원인이 우리에게 없었다면, 포스트모텀만으로 단단한 구조를 만들 수 있을까?

원인 모를 문제와 패배가 정해진 싸움

한가롭던 어느 날, 프론트엔드 채널에 이슈 리포트가 들어왔습니다. 랜딩페이지에 접속하면 몇 초 후에 스타일이 갑자기 모두 사라지는데, 시크릿 모드로 접속하면 잘 뜬다는 이상한 이슈였습니다.
처음에는 일시적인 네트워크 / 캐싱 문제라고 생각했습니다. CloudFront에서 Cache Invalidation을 하고 조금 기다리면 해결되리라 생각했습니다. 하지만 그렇지 않았고, 이 이슈로 종일 고통받을 것을 그땐 알지 못했습니다.
디버깅을 계속하던 중 이상하게 Markdown으로 렌더링하는 페이지들(약관 등)은 문제가 없었습니다. 국제화(i18n)이 문제인가 싶어 언어의 분기, 파싱 로직 등도 바꿔봤지만 소용이 없었습니다. 시간은 벌써 3시간이 흘렀고, 불규칙하게 나타나는 이슈 때문에 고객이 영향을 받고 있다는 생각에 두려움이 커졌습니다.
혹여나 하는 마음으로 오류가 난 HTML과 정상적인 HTML을 구해 비교해봤고, 콜스택까지 모두 들여다보고 나서야, 스타일 라이브러리에 외부 서드파티가 간섭해 문제가 발생했음을 알게 되었습니다.
그렇게 찾아낸 이슈의 원인은, 내부에서 작성된 코드 혹은 인프라의 문제가 아니었던 것입니다.
이제는 기억 속 흐릿하게 남은 초기의 서비스 랜딩페이지
이슈의 원인은 사용자가 설치한 Chrome Extension에서 발생했습니다. Chrome Extension은 자동으로 업데이트가 되는데, 최신 버전에 버그가 발생해 해당 Extension에서 사용하는 스타일 라이브러리와 같은 것을 사용한다면 강제로 덮어씌워져 스타일이 깨졌던 것이었습니다.
해당 팀에 해결해서 9시간만에 이슈를 종결했지만, 제게는 극심한 허무함과 분노만이 남았습니다. 먼저 원인을 파악하는 것이 어려웠습니다. 서비스가 정상적으로 동작하지 않지만 이것이 내부의 문제인지, 외부의 문제인지, 장애 상황은 맞는지를 판단하기 어려웠기 때문입니다.
사실 제가 생각했던 가장 큰 문제는, 문제의 원인이 외부에 있다는 것을 알기까지 무려 4~5시간이 소요되었다는 점이었습니다. 만약 실제 장애 상황이었다면 ‘SLA(Service-Level Agreement)’는 아득히 초과했을 것입니다.
개발을 한다면 문제를 피할 수는 없습니다. 하지만 매번 돌다리를 두들기고 싶지는 않았습니다. 최소한 지금보다는 덜 두려워하며 문제를 마주하고 싶었습니다. 분노, 후회 등의 감정을 담아 글을 남기기 시작했고…
그렇게 프론트엔드의 Notion에  Player Unknown’s Bug’ 라는 페이지가 태어났습니다.

분노의 이슈 정리, 그리고 회고

눈치를 채신 분들도 계시겠지만, “Player Unknown’s Bug”라는 이름은 당시에 유행하던 배틀그라운드에서 차용해왔습니다. 말 그대로 ‘원인을 알 수 없는’(Player Unknown’s) ’버그’(Bug) 라는 의미에 잘 맞는 것 같습니다.
처음으로 페이지를 만들고, 이슈를 정리한 뒤, 팀 회고를 진행했습니다. 간단히 얼개만 보면… 너무 길어보이면 여기를 누르세요 →
랜딩페이지는 Gatsby를 기반으로 하고 있으며, Emotion으로 스타일링 하고 있음
D+0시간 — 이슈 리포팅 받음. 네트워크, 캐시 문제라고 판단해서 CF에서 Cache Invalidate함. 효과 없음.
이유: 스타일 문제, 시크릿 모드에서 잘 뜬다는 점 때문에
D+2시간 — 다른 페이지 중 이용약관 페이지는 잘 나오는 것을 확인
해당 페이지는 Markdown으로 렌더링 → 국제화(i18n)을 위해 i18next 를 사용하지 않음
가설 1) 렌더링 준비 과정에서 설정 언어가 바뀌어 i18next에 오류가 발생했다
가설 2) Fallback 언어를 설정했음에도 언어를 찾지 못해 i18next에 오류가 발생했다
놓친 부분: i18next에서 오류가 났다면 코드 구조상 언어 토큰이 뜨거나 아예 크래시가 났을 것임. 스타일과의 연관성이 적을 수 있음을 판단했어야 함.
D+3시간 — i18next 관련 이슈 대응. 효과 없음.
서드파티 라이브러리 중 document.cookielanguage 값을 계속 바꾸는 것이 있었음. 랜딩페이지에서는 해당 값을 참조해 언어 분기함 → 언어 분기를 위해 참조하는 쿠키값을 변경함
window.navigator.language 에서 가져온 값과 i18next 에 선언한 값이 달라서 오류가 발생한 것은 아닐지 추측
같은 Chromium임에도, Samsung Browser는 ko-KR / Chrome은 ko를 반환함
랜딩페이지의 코드는 해당 값이 ISO 639-1에 맞춰져있다고 확정하고 작성함 → 그렇지 않다면 오류가 날지도? (그렇지 않음)
i18next-browser-language-detector 패키지를 설치
i18nextavailableLng 옵션을 활성화
D+4시간 — 시크릿 모드의 HTML과 일반 모드의 HTML 결과물 비교.
Emotion의 스타일 생성 로직이 모두 깨져 <style data-emotion> 이 비어있음. 조금 더 보니까 처음에 생성이 된 후에 빈 값으로 치환됨.
디버깅 대상을 국제화 라이브러리 → 스타일 라이브러리로 전환
랜딩페이지에 종속성을 갖는 외부 요인 중 Emotion을 쓸만한 것들은 Intercom, Chrome Extension 정도로 추려냄. 하나씩 비활성화해보며 점검
Chrome Extension, 그중에서도 Loom의 이슈인 것을 확인, Loom 팀에 이슈 리포팅
D+9시간 — Loom 팀에서 Chrome Extension 변경 버전 배포 완료. 이슈 종료.
Loom의 Chrome Extension에서 사용하는 스타일을 마운트하는 로직에 버그가 있어 Chrome Extension의 HTML이 아니라, 사용자가 보고 있는 페이지에 덮어씌우는 문제가 있었다고 함.
삽질하면서 알게 된 것: IETF의 BCP-47 스펙 문서 (Language Tags 관련)
아쉬웠던 것: Chrome Extension은 별도의 설정이 없으면 시크릿 모드에서 비활성화 됨. Chrome Extension이 서비스에 영향을 줄 것이라고는 생각을 못했던 것이 아쉬움 → 대응 플로우에 해당 내용 추가
마치 ‘사이버 바디랭귀지’를 하듯이 Loom팀과 이야기했던 기억이 납니다

The Player Unknown’s Bug (PUB)

회고하며 이슈의 원인, 찾아보며 부가적으로 알게 된 것, 오판한 지점 등을 이야기했습니다. 팀원들은 제게 궁금한 내용을 물어봐 주시기도 했고, 개선할 수 있는 지점들도 짚어주셨습니다. 회고에서 나온 내용을 정리해 장애 대응 프로세스에 체크리스트를 추가했습니다.
저는 이 과정에서 많은 것을 느꼈습니다. 팀원들이 이슈 대응 과정에서 느꼈던 어려움과 스트레스에 공감해주었습니다. 몰랐던 것을 정리하며 다시 알게 되니 재미가 있었고, 무엇보다도 미지의 오류를 차근차근 뜯어볼 수 있는 체크리스트를 갖게 되어 좋았습니다.
비슷한 경우에 이렇게 기록으로 남기고 회고하면 좋겠다는 이야기를 팀에 전했고, 그렇게 PUB는 정식으로 프론트엔드 팀 문화에 스며들게 됩니다.
현재의 PUB는 AB180 개발 조직에서 진행하는 포스트모템을 보완하는 형태로 진행하고 있습니다. 사내에서 진행하는 포스트모템이 Resolution과 Action Items이 중심이라면, PUB는 Lesson Learn, 이슈 대응에 대한 심리적 안정감 형성, 알 수 없는 문제에서 먼저 확인할 요인들을 정리하는 것을 목표로 합니다.
정해진 규칙이나 양식은 없지만, 경험적으로 쌓인 것들은 아래와 같습니다.
원인을 몰라서 해결이 오래 걸린 경험을 기록합니다 (외부 요인, 지식 없음 등…)
내용은 가급적 시간순으로 작성하고, 해당 시도를 한 이유를 작성합니다
작성을 끝내면 Slack 팀 채널에 공유하고, 주간 회고를 하며 대응이 적절했는지 묻습니다

위너에게는 치킨 디너를

Player Unknown’s Bug를 만들기 시작한 이후에도 신기한 이슈를 만나고 대응했습니다. 당장 기억나는 것들은…
Safari 13 버전 이하의 Intl 모듈은 Deprecated 된 IANA 스펙을 사용하기 때문에, dayjs 라이브러리에서 Timezone 오류가 생길 수 있다
react-queryrecoil을 함께 쓸 때, selector의 로직이 복잡해 메모리를 많이 잡아먹으면 react-query 가 무한 루프에 빠질 수 있다
lodash.flatMap()terser 의 minify 과정 중에서 충돌이 발생, 일정 확률로 크래시가 날 수 있다
솔직히 처음에는 분노의 감정으로 날려 만들었지만, 팀의 문화로 조금씩 스며들면서 Player Unknown’s Bug는 조금 더 재밌는 양상을 보이게 됩니다. 회고하며 새롭게 알게 된 내용, 잘못된 곳을 파보면서 그 자체가 작은 ‘지식 공유 세션’이 되기 시작했습니다.
그걸 보며 ‘정보를 공유하는 문화를 조금 더 키워볼 수 있을까?’ 생각이 들었습니다. 지금은 실험적으로 새롭게 알게 된 것을 수시로 올릴 수 있는 Slack 채널을 만들어 운영하고 있습니다. 별도의 터치 없이도 수시로 각자의 경험이 올라오며 이야기를 나눌 수 있습니다.
비용 연동 작업 중 dayjs(null)을 넣었을 때 발생한 이슈를 공유해주신 준기님
가야할 길은 여전히 멉니다. 이슈 대응은 돌다리를 덜 짚게 되었지만, ‘지식을 공유하는 문화’로 확장하는 일은 사방이 두들겨야 할 돌다리 투성이입니다. 몇 번 자빠질지도 모릅니다. 그럼에도 시간이 흐름에 따라 더 많은 이야기를 들려드릴 수 있을 것입니다. 개인적으로 기대가 됩니다.
위에서 이야기를 했지만, 아예 문제를 만나지 않는 것은 불가능한 일입니다. 그렇기에 문제를 제대로 파악하고, 대응하고, 회고해야합니다. 장애를 대응한 팀 전체의 심리적 안정감을 높여야하고, 그러기 위해서는 더 많은 이야기를 나눠야합니다.
그렇지 않았을 때, 문제는 반복되며, 조직 내에 ‘문제 = 말하면 안 되는 것’이라는 생각이 자리잡을지도 모릅니다.
화를 내며 만든 페이지 하나를 통해 제가 경험한 것은, 정보의 공유를 통해 조직의 심리적 안정감을 만들고, 심지어 높이는 것이 가능하다는 것이었습니다. 진지하게 대응하되 피할 수 없으니 즐길 수 있게 된 것이죠.
여러분의 조직에서는 어떻게 정보를 공유하고 심리적 안정감을 만들어가고 계신가요? 어쩌면 생각보다 별 거 아닌 것으로 시작할 수 있을지도 모릅니다.
그럼 저는 이만 치킨 디너 하러 가보겠습니다.
ᴡʀɪᴛᴇʀ
Chanhee Lee @hiddenest Frontend Software Engineer
유니콘부터 대기업까지 쓰는 제품. 같이 만들어볼래요? 에이비일팔공에서 함께 성장할 다양한 직군의 동료들을 찾고 있어요! → 더 알아보기