안녕하세요! 그로스로그 입니다!😊 🌱
푸시 알림을 처음 붙이며 공식 문서를 파고든 기록, 그리고 시크릿탭에서만 간헐적으로 터지던 에러를 끝까지 쫓아간 기록. 1기 4회차 두 편입니다.
성장일지는 멤버들이 2주에 한 번씩 자신의 성장을 기록하는 활동이에요. 매 회차 운영진이 모든 글을 읽고, 그중 특히 마음에 남은 글을 함께 골라 큐레이션합니다.
이번 회차는 문서와 로그를 읽어내는 힘이 그대로 드러난 두 편이었습니다.
01
📲 FCM 푸시 알림 (코틀린, 스프링부트)
오연수 님
스프링부트와 코틀린으로 모바일 앱에 FCM 푸시 알림을 붙이는 전 과정을 담았습니다.
설정 코드부터 실제로 서버가 보내는 요청 페이로드까지 그대로 실려 있어서, 같은 작업을 해야 하는 사람이 화면을 옮겨가며 따라 할 수 있어요.
특히 인상적인 건 알림 메시지(notification)와 데이터 메시지(data)의 차이를 공식 문서에서 직접 찾아내 풀어가는 대목입니다. 처음엔 둘의 구분을 모른 채 시작했다가, 문서를 읽고 나서야 왜 앱이 켜져 있을 때와 꺼져 있을 때 동작이 다른지를 이해하게 되는 흐름이 그대로 남아 있습니다.
푸시 알림은 대부분의 앱이 한 번은 붙이게 되는 기능인데, 막상 처음 하면 생각보다 헤맵니다.
이 글은 Firebase Cloud Messaging(FCM)을 스프링부트+코틀린 서버에 붙이는 과정을 순서대로 기록했습니다. 설정, 인증, 실제 전송 요청까지요. 무엇보다 요청 페이로드가 그대로 실려 있어서 내 코드와 대조해볼 수 있습니다.
조금 더 들어가 보면
푸시 알림이 왜 별도 서비스를 거쳐야 하는지부터 짚고 가면 이해가 쉽습니다.
우리 서버가 사용자 폰에 직접 메시지를 보낼 수는 없습니다. 폰은 IP가 계속 바뀌고, 대부분의 시간 동안 잠들어 있고, 앱도 꺼져 있으니까요. 그래서 OS 제조사가 운영하는 중계소를 거칩니다. 안드로이드는 FCM, iOS는 APNs예요. 폰은 이 중계소와만 연결을 유지하고, 우리 서버는 중계소에 "이 사용자에게 이 메시지 전해줘"라고 부탁하는 구조입니다.
여기서 글이 짚어낸 notification과 data의 차이가 중요해집니다. 이게 FCM 입문자가 가장 많이 걸려 넘어지는 지점이에요.
notification 메시지를 보내면 알림 표시를 OS가 알아서 해줍니다. 앱이 꺼져 있어도 알림창에 뜹니다. 편하죠. 대신 앱이 화면에 떠 있는 상태(foreground)에서는 OS가 자동 표시를 하지 않고 앱에게 넘깁니다. "앱이 꺼져 있을 때는 알림이 오는데 켜놓으면 안 온다"는 증상이 여기서 나옵니다.
data 메시지는 순수한 데이터만 전달합니다. 알림을 띄울지, 어떤 모양으로 띄울지는 전부 앱 코드가 결정해요. 손이 더 가는 대신 동작이 예측 가능하고, 앱 상태와 무관하게 일관되게 처리할 수 있습니다.
그래서 실무에서는 data 메시지만 쓰고 알림 표시를 직접 구현하는 쪽을 택하는 팀이 많습니다. "테스트할 때는 됐는데 실사용에서 안 온다"는 문제를 근본적으로 없애려는 거죠.
이 글의 가치는 그 차이를 문서에서 직접 찾아내 이해한 과정이 남아 있다는 점입니다. 블로그 글을 복사해 붙였으면 동작은 했겠지만, 왜 그런지는 몰랐을 거예요.
큐레이터 노트
공식 문서를 읽고 스스로 답을 찾은 기록이라는 점에서 뽑았습니다.
기능 하나를 붙일 때 검색해서 나온 블로그를 따라 하면 대개 30분이면 끝납니다. 그런데 그렇게 하면 동작이 예상과 다를 때 아무것도 할 수 없어요. 내가 왜 그 코드를 썼는지 모르니까요.
이 글은 알림 메시지와 데이터 메시지의 차이라는, 문서에 있지만 대충 읽으면 놓치는 부분을 붙들고 늘어졌습니다. 그 결과 설정값이 아니라 이해가 남았어요.
그리고 요청 페이로드를 실제로 남겨둔 것도 좋았습니다. 푸시가 안 올 때 제일 답답한 게 "내가 뭘 잘못 보내고 있는지" 모르는 상황인데, 정상 동작하는 페이로드가 하나 있으면 비교만으로 원인을 좁힐 수 있거든요.
02
🕵️ 트러블슈팅 — 서버 배포 후 간헐적 ChunkLoadError 발생 원인
변기원 님
Next.js 프로젝트를 배포한 뒤, 시크릿탭에서 그것도 처음 접속할 때만 간헐적으로 터지는 ChunkLoadError를 추적한 기록입니다.
"잘되다가 안 되다가 이유 없이 또 잘되는" 유형의 버그라 재현부터가 어려웠어요. 글은 가설을 하나씩 세우고 지워나가는 과정을 그대로 보여줍니다.
최종 원인은 서버 두 대가 각자 빌드하면서 청크 파일명의 해시가 서로 달라진 것이었습니다. 한 서버에서 받은 HTML이 다른 서버에 없는 파일을 요청하게 되니 404가 나고, 브라우저는 ChunkLoadError를 던진 거죠.
개발자를 가장 지치게 하는 버그는 간헐적인 버그입니다. 항상 터지면 차라리 낫습니다. 재현이 되니까요.
이 글의 증상이 딱 그렇습니다. 시크릿탭에서, 그것도 처음 접속할 때만, 가끔. 조건이 이렇게 붙으면 "내가 잘못 본 건가" 싶어지기 시작합니다.
조금 더 들어가 보면
요즘 프론트엔드 프레임워크는 자바스크립트를 하나의 거대한 파일로 만들지 않습니다. 화면별로 잘게 쪼갠 조각(chunk)으로 나눠두고, 사용자가 그 화면에 갈 때 필요한 조각만 내려받게 해요. 첫 로딩을 빠르게 하려는 최적화입니다.
이때 각 조각 파일에는 main.a3f9c1.js 같은 해시가 붙습니다. 내용이 바뀌면 해시도 바뀌게 해서, 브라우저가 옛날 파일을 캐시에서 잘못 꺼내 쓰는 걸 막는 장치예요.
문제는 이 해시가 빌드할 때마다 계산된다는 점입니다.
서버가 두 대이고 각각 따로 빌드를 돌리면 어떻게 될까요. 같은 소스코드인데도 빌드 환경의 미세한 차이로 해시가 다르게 나올 수 있습니다. 그러면 이런 일이 벌어집니다.
1. 사용자가 A 서버에서 HTML을 받는다 → 그 HTML은 main.a3f9c1.js를 요청하라고 적혀 있다
2. 다음 요청이 로드밸런서에 의해 B 서버로 간다
3. B 서버에는 main.b7e2d4.js밖에 없다 → 404
4. 브라우저는 필요한 조각을 못 받았다며 ChunkLoadError를 던진다
시크릿탭에서만 잘 보이는 이유도 여기서 설명됩니다. 평소 브라우저는 이전에 받은 파일을 캐시에 갖고 있어서 문제가 가려지는데, 시크릿탭은 캐시가 비어 있으니 매번 새로 요청하거든요. "처음에만" 터지는 것도 같은 이유입니다.
해결 방향은 두 갈래입니다. 한 번만 빌드해서 그 결과물을 모든 서버에 배포하거나, 정적 파일을 서버가 아닌 공용 저장소나 CDN 한 곳에서 서빙하는 겁니다. 어느 쪽이든 원칙은 같아요. 모든 서버가 똑같은 파일을 보게 만드는 것.
이 버그가 교육적인 이유는, 로컬에서도 스테이징에서도 절대 안 나타난다는 점입니다. 서버가 한 대면 문제 자체가 존재하지 않으니까요. 배포 구조가 만드는 버그라, 코드만 봐서는 평생 못 찾습니다.
큐레이터 노트
이 글의 진짜 가치는 결론이 아니라 가설을 지워나가는 과정에 있습니다.
간헐적 버그를 만나면 대부분 "일단 다시 배포해보자"로 넘어갑니다. 그러면 우연히 증상이 사라지고, 원인은 그대로 남죠. 이 글은 그러지 않았어요. 시크릿탭에서만 난다는 단서, 처음에만 난다는 단서를 붙들고 왜 그 조건일 때만인지를 끝까지 물었습니다.
그리고 답은 코드가 아니라 배포 구조에 있었습니다. 3회차의 nginx 버퍼링 건과 같은 결론이에요. 같은 분이 연달아 이런 글을 쓰셨다는 게 우연이 아니라고 생각합니다.
"잘되다가 또 안 되다가 이유없이 잘되다가"라는 원문의 표현이 이 버그의 성격을 정확히 담고 있어요. 그 상태에서 포기하지 않은 기록이라 큐레이션했습니다.
💡 두 편을 겹쳐 읽으며
한 편은 새 기능을 붙이는 글이고, 한 편은 버그를 잡는 글입니다. 정반대 상황인데 문제를 푼 방법이 같습니다.
남이 정리해둔 답을 찾지 않고, 원본을 직접 읽었다.
오연수 님은 FCM 공식 문서를 읽고 notification과 data의 차이를 이해했습니다. 검색 결과를 따라 했으면 동작은 했겠지만 왜 그런지는 몰랐을 거예요. 변기원 님은 서버 로그와 브라우저 동작을 직접 관찰해서 해시 불일치라는 원인에 도달했습니다. 스택오버플로에서 ChunkLoadError를 검색했다면 "캐시를 지워보세요" 같은 답만 나왔을 거고요.
원본이 뭐냐만 달랐습니다. 한쪽은 공식 문서, 한쪽은 로그와 실제 동작.
이번 회차에서 가져갈 것
1. 문서는 검색보다 느리지만 오래 간다. 블로그를 따라 하면 30분, 문서를 읽으면 2시간이 걸릴 수 있습니다. 그런데 예상과 다르게 동작할 때 대응할 수 있는 건 문서를 읽은 쪽입니다. 한 번은 쓰게 될 기능이라면 처음에 제대로 읽어두는 편이 남습니다.
2. 조건이 붙은 버그는 그 조건이 힌트다. "시크릿탭에서만", "처음에만" 같은 단서는 성가신 게 아니라 범위를 좁혀주는 정보입니다. 왜 하필 그 조건일 때만인지를 물으면 대개 원인 근처까지 갑니다.
3. 서버가 두 대가 되는 순간 새로운 종류의 버그가 생긴다. 세션도(1회차), 정적 파일 해시도(이번 회차) 서버가 한 대일 땐 존재하지 않던 문제였습니다. "모든 서버가 같은 것을 보고 있는가" — 분산 환경에서 늘 확인해야 하는 질문입니다.
1기 4회차입니다. 이때쯤 되면 멤버들이 성장일지에 무엇을 쓸지 감을 잡기 시작했어요. 처음엔 "무슨 이야기를 써야 하나" 망설이던 분들이, 이번 주에 붙들고 있던 문제를 그대로 적어 내려가기 시작합니다.
사실 그게 가장 좋은 소재입니다. 특별한 걸 만들어내지 않아도, 지금 막힌 지점을 적으면 그게 곧 기록이 되니까요.
좋은 글 남겨주신 오연수 님, 변기원 님께 감사드립니다. 그리고 이번 주에도 어딘가에서 간헐적 버그와 씨름하고 계실 모든 멤버분들께도요! 😊
다음 큐레이션으로 다시 찾아오겠습니다! 🌱
- 에디터 · 성장일지 큐레이터 -
※ 본 큐레이션은 각 저자가 공개한 글을 소개하는 것이며, 모든 원문의 저작권은 저자에게 있습니다. 저자 본인의 요청이 있을 경우 즉시 수정 또는 삭제합니다.
'성장일지' 카테고리의 다른 글
| [GL 1기] AWS 프리티어 12개월 제대로 쓰기, 그리고 JSP 404 추적 (성장일지 6회차 큐레이션) 🌱 (0) | 2026.08.07 |
|---|---|
| [GL 1기] React-Flask 배포 시행착오와 환경별 설정 분리 (성장일지 5회차 큐레이션) 🌱 (0) | 2026.08.07 |
| [GL 1기] EC2에서만 멈추는 스트리밍, 500줄짜리 검색 쿼리 (성장일지 3회차 큐레이션) 🌱 (0) | 2026.08.07 |
| [GL 1기] 인생 2막 개발자의 프로젝트 회고, 그리고 방통대 졸업 전략 (성장일지 2회차 큐레이션) 🌱 (0) | 2026.08.07 |
| [GL 1기] Redis 세션 클러스터링과 시각적 회귀 테스트 (성장일지 1회차 큐레이션) 🌱 (0) | 2026.08.07 |