H-1: 에러 모니터링 ① — 오류가 우리에게 오게 하는 일
목차 45
안녕하세요, 홍순구 튜터입니다.
질문 하나로 시작할게요.
방금 여러분이 만든 앱이 누군가의 화면에서 터졌는데, 재훈이라는 사용자가 피드를 열었더니 게시물 대신 "문제가 생겼어요"가 떴어요. 여러분은 그걸 언제 알게 될까요?
잠깐 생각해보세요. 바로 알까요, 몇 분 뒤일까요, 아니면 다음 날 누가 문의를 넣어야 알게 될까요?
정답은 셋 다 아닙니다. 영영 모릅니다.
재훈이는 화면을 닫고 다시 열어보다가 그래도 안 되면 앱을 지우겠죠. 그런데 우리에게 연락하는 사람은 백 명 중 한 명도 안 되고, 나머지 아흔아홉 명은 조용히 떠나서 우리는 아무것도 못 본 채로 다음 기능을 만듭니다.
지난 시간까지 우리가 재본 건 전부 우리 노트북에서였는데, 콘솔을 열어둔 채로 작업하니까 뭔가 터지면 빨간 글씨가 바로 보였지만 사용자 화면에는 그 콘솔이 없습니다. 정확히는, 있는데 아무도 안 엽니다.
오늘은 그 간격을 메우는데, 남의 화면에서 벌어진 일을 우리에게 오게 만드는 거예요.
오늘의 여정
1. 지금은 오류가 나도 아무 데도 안 남는다
2. 보내는 쪽을 켠다 → 우리가 직접 받아본다
3. 봉투를 열어본다 → 무엇이 담겨 나가는지
4. 그런데 정작 진짜 오류는 안 올라간다 → 0건
5. 경계를 잇는다 → 화면은 그대로인데 봉투만 갈린다
💡 오늘 수업의 핵심 — "잡아주는 것과 알려주는 것은 다릅니다. 우리 앱은 오류를 잘 잡고 있었고, 잘 잡았기 때문에 아무도 몰랐습니다"
🎯 학습 목표
- 지금 앱에서 오류가 났을 때 그 사실이 어디에도 안 남는다는 것을 직접 확인하고, 왜 그것이 문제인지 설명할 수 있습니다.
- 오류 추적 도구를 앱에 붙이고, 브라우저가 실제로 무엇을 담아 보내는지 봉투를 열어 읽습니다.
- 화면에 오류가 떴는데도 아무것도 안 나가는 상황을 재현하고, 그 원인을 찾아 경계를 이어줍니다.
Step 1: "오류가 나도 우리는 모릅니다"
먼저 터뜨려봅니다
말로 하면 안 와닿으니 직접 터뜨려보겠습니다.
연습용 API 서버를 끄세요. 켜져 있다면 그 터미널에서 Ctrl+C를 누르면 되는데, 그다음 앱을 띄운 뒤 홈 화면을 엽니다.
npm run dev
홈 화면이 "피드를 불러오는 중이에요…"를 보여주다가, 몇 초 뒤에 이렇게 바뀝니다.
문제가 생겼어요
서버에 연결할 수 없어요
홈으로
C-2에서 만든 오류 화면이 제 할 일을 했는데, C-6에서 붙인 안내 문구도 우리말로 잘 나오고 있고요. C-7에서 throwOnError를 켜둔 덕분에 읽기 실패가 화면 안에서 뭉개지지 않고 이 화면까지 올라왔습니다.
여기까지는 우리가 지난 회차들에서 공들여 만든 그대로예요. 아무 문제 없습니다.
그런데 이 일이 어디에 남았나요
질문을 바꿔봅니다. 그럼 방금 그 오류가 어디에 기록됐을까요?
찾아봅시다. 개발자 도구 콘솔에는 빨간 글씨가 있는데, 그게 우리 노트북이니까요.
그 콘솔은 재훈이 브라우저에 있는 것인데, 우리 것이 아니라 재훈이가 탭을 닫는 순간 같이 사라집니다.
서버 쪽은 어떨까요. 연습용 API 서버는 아예 꺼져 있었는데, 요청을 받은 적이 없으니 남길 것도 없죠. 설령 켜져 있었어도, 브라우저가 그린 화면에서 무슨 일이 있었는지는 서버가 알 방법이 없습니다.
⚠️ 여기가 프론트엔드의 고약한 점입니다. 백엔드는 코드가 우리 서버에서 돌아서 터지면 우리 로그 파일에 쌓이는데, 프론트엔드 코드는 남의 기계에서 돌기 때문에 우리가 안 보는 곳에서 터지고 우리가 안 보는 곳에서 사라집니다.
손 확인이 닿지 않는 곳
F-1에서 비슷한 이야기를 한 번 했는데, 손으로 눌러보는 확인에는 한계가 있다고요. 그때는 화면 수가 늘어나는 게 문제였는데, 눌러볼 곳이 너무 많아진다는 뜻이었죠.
오늘 문제는 결이 다릅니다. 눌러볼 수가 없어요. 재훈이의 화면은 재훈이만 볼 수 있으니까요. 테스트를 아무리 촘촘히 써도 우리가 상상하지 못한 조합에서 터지는 것은 못 막는데, 브라우저 버전이 다르거나 확장 프로그램이 끼어들거나 네트워크가 중간에 끊기거나요.
그래서 필요한 게 하나 더 있습니다. 터졌다는 사실 자체를 우리에게 배달해주는 장치요.
💡 한 줄 정리
우리 앱은 오류를 잘 잡고 있었고 화면도 정중하게 바뀌었지만, 잡아서 예쁘게 보여준다는 것과 그 일이 있었다고 알려준다는 것은 완전히 다른 이야기이고 지금 우리에게는 뒤엣것이 통째로 없습니다.
🙋 학생 질문 — "튜터님, 그냥 사용자한테 오류 나면 알려달라고 하면 안 되나요?"
안 됩니다. 정확히는, 되긴 하는데 거의 아무 소용이 없어요.
업계에서 자주 쓰는 어림값이 있는데, 문제를 겪은 사람 중 실제로 신고하는 비율은 1퍼센트 남짓이라고들 해요. 회사마다 앱마다 다르니 이 숫자 자체를 외우실 필요는 없고, 요점은 압도적 다수가 조용히 떠난다는 겁니다.
게다가 신고가 들어와도 쓸모가 제한적인데, "며칠 전 저녁에 들어갔는데 안 됐어요"라는 문장으로 우리가 할 수 있는 게 별로 없거든요. 어느 화면인지, 어떤 브라우저인지, 무엇을 누른 다음이었는지가 다 빠져 있죠.
오늘 붙일 장치는 그 빠진 것들을 자동으로 채워서 보내니, 사람이 기억해서 적어주길 기다리지 않아도 됩니다.
Step 2: "보내는 쪽을 켭니다"
어디로 보낼지부터 정합니다
오류를 배달하려면 받는 곳이 있어야 합니다.
실무에서는 이런 걸 전문으로 하는 서비스를 쓰는데, 우리 앱에서 오류가 나면 그 회사 서버로 보내지고 우리는 그 회사가 만든 화면에서 목록을 보는 방식이에요.
그런데 오늘 우리는 받는 쪽을 직접 만들어서 씁니다. 이유가 둘입니다.
하나는 계정을 만들 필요가 없다는 것. 다른 하나가 더 중요한데, 무엇이 나가는지 우리 눈으로 볼 수 있다는 겁니다. 남의 서비스를 쓰면 브라우저가 보낸 내용은 그 회사 서버 안으로 들어가버려서 우리는 정리된 화면만 보게 되는데, 오늘은 그 봉투를 직접 뜯어볼 겁니다.
받는 쪽은 저장소에 이미 준비돼 있습니다. 새 터미널을 열어 띄우세요.
node apps/api-stub/sentry-stub.mjs
오류 받는 연습용 서비스가 http://localhost:9000 에서 기다리고 있어요.
받은 것을 다시 보려면 http://localhost:9000/envelopes
이 서비스도 연습용 API 서버와 똑같이 의존성이 하나도 없는데, Node에 들어 있는 것만으로 만들었어요.
⚠️ 파일이 server.mjs와 따로 있고 포트도 다르다는 점을 눈여겨보세요. 일부러 그렇게 했는데, 진짜 세상에서도 오류를 받아주는 곳은 우리 API 서버와 다른 회사가 다른 주소에서 운영하거든요. 한 파일에 몰아넣으면 그 사실이 가려집니다.
도구를 들입니다
이제 보내는 쪽입니다. 업계에서 가장 널리 쓰이는 도구를 깔아볼게요.
npm i -D @sentry/react
React 용이라고 되어 있지만 안에 브라우저용 공통 코드가 들어 있고, React에 맞춘 도구가 몇 개 더 얹혀 있는 구성입니다.
DSN — 어디로 보낼지 적는 한 줄
이 도구는 보낼 주소를 DSN이라는 한 줄짜리 문자열로 받는데, 생김새는 이렇습니다.
http://demopublickey123@localhost:9000/7
demopublickey123 공개 키 — 받는 쪽이 확인하지 않는다
localhost:9000 보낼 주소
7 프로젝트 번호
주소가 우리 연습용 서비스를 가리키고 있죠. 앞에 붙은 공개 키는 진짜 서비스에서는 프로젝트를 만들면 발급받는 값이지만, 받는 쪽이 그 값을 확인하지 않기 때문에 우리는 아무 글자나 지어내서 쓸 수 있어요.
다만 "아무 글자나"에 함정이 하나 있습니다. 뒤에서 다시 보겠습니다.
설정을 한곳에 모아둡니다.
// apps/web-spa/src/lib/monitoring.ts
import * as Sentry from '@sentry/react';
// DSN 은 "어디로 보낼지" 하나만 정한다. 우리는 연습용 서비스를 우리 노트북에 띄워두고
// 그쪽을 가리킨다. 공개 키 자리는 서버가 확인하지 않지만 모양은 검사한다
// (영문·숫자·밑줄만 — 하이픈을 넣으면 SDK 가 DSN 을 통째로 버린다).
//
// 이 값은 브라우저로 그대로 실려 나간다. VITE_ 로 시작하는 값은 전부 그렇다.
// 그래서 DSN 은 비밀이 아니다 — 비밀이었다면 여기 두면 안 됐다.
const DSN = import.meta.env.VITE_SENTRY_DSN ?? 'http://demopublickey123@localhost:9000/7';
export function startMonitoring(): void {
Sentry.init({
dsn: DSN,
// 오류만 본다. 속도를 재는 일은 나중에 따로 다룬다.
tracesSampleRate: 0,
// 어느 환경에서 온 오류인지 갈라 볼 수 있게 표시해둔다.
environment: import.meta.env.DEV ? 'development' : 'production',
});
}
D-10에서 배운 것이 여기서 그대로 걸리는데, VITE_로 시작하는 값은 브라우저로 실려 나가죠. 그러니 DSN은 비밀이 아니고, 누구나 우리 앱 파일을 열어서 읽을 수 있어요.
처음 보면 이상하게 느껴질 수 있는데, 아무나 우리 프로젝트로 오류를 보낼 수 있다는 뜻이니까요. 실제로 그렇지만, 이 값은 자물쇠가 아니라 주소예요. 우편함 주소를 안다고 우편함을 열 수 있는 건 아니듯이, DSN을 안다고 우리가 받은 오류 목록을 볼 수 있는 건 아닙니다.
앱에서 가장 먼저 켭니다
이제 앱이 시작할 때 이걸 부릅니다.
// apps/web-spa/src/main.tsx
import { startMonitoring } from './lib/monitoring';
// 오류를 알아채는 일은 앱에서 가장 먼저 켠다.
// 늦게 켜면 그 전에 터진 것은 아무도 못 본다.
startMonitoring();
const rootElement = document.getElementById('root');
순서가 중요합니다. 화면을 그리기 전에 켜야 하는데, 그리는 도중에 터지는 것도 잡아야 하니까요.
정말 나가는지 확인합니다
붙였으면 확인해야죠. 개발자 도구 콘솔을 열고 한 줄 실행해보세요.
Sentry.captureException(new Error('연습용 오류입니다'))
콘솔에서 Sentry를 못 찾는다고 나오면, 개발 중에 잠깐 확인할 수 있게 main.tsx에서 window에 붙여두거나, 아래처럼 모듈을 직접 불러 쓰면 됩니다.
const S = await import('@sentry/react'); S.captureException(new Error('연습용 오류입니다'))
그리고 연습용 서비스를 띄워둔 터미널을 보세요.
[오류받음] 2,128 B · 0d83ac78cf7b4f608814a0f938f02a45
Error: 연습용 오류입니다 ← …
도착했습니다. 우리가 지어낸 공개 키로, 우리 노트북에 띄운 서비스로요.
⚠️ 바이트 수는 여러분 화면에서 다르게 나오는데, 오류가 어디서 났느냐에 따라 딸려오는 정보량이 크게 달라지거든요. 같은 도구로 재도 몇백 바이트에서 만 바이트 가까이까지 왔다 갔다 하니까, 숫자 자체보다 한 건에 이 정도가 오간다는 감각만 가져가세요.
⚠️ 주소를 틀리게 적으면 어떻게 될까요
여기서 아까 미뤄둔 함정을 봅니다. 공개 키에 하이픈을 넣어보세요.
http://demo-public-key@localhost:9000/7
그리고 오류를 다시 보내봅니다. 연습용 서비스 터미널에는 아무것도 안 찍힙니다.
콘솔을 보면 이유가 있습니다.
Invalid Sentry Dsn: http://demo-public-key@localhost:9000/7
공개 키에는 영문·숫자·밑줄만 쓸 수 있는데, 하이픈이 들어가면 DSN을 통째로 버려요.
그런데 진짜 고약한 건 이겁니다. 보내는 걸 확실히 하려고 쓰는 함수가 하나 있는데요.
await Sentry.flush(4000)
// true
true를 돌려줍니다. 보낼 게 하나도 없으니 "다 보냈다"가 참인 거죠. 틀린 답은 아니지만, 이 값만 보고 있으면 잘 돌아가는 줄 압니다.
이 과목에서 계속 만나온 모양인데, 아무 소리도 안 난다는 것이 잘 되고 있다는 뜻은 아니에요. 확인은 보내는 쪽이 아니라 받는 쪽에서 해야 합니다.
확인이 끝나면 하이픈을 지워 되돌리세요.
💡 한 줄 정리
보내는 쪽은 DSN 한 줄로 정해집니다. 그 값은 브라우저로 실려 나가니 비밀이 아니고, 잘못 적어도 앱은 멀쩡히 돌아가면서 조용히 아무것도 안 보내기 때문에, 붙인 다음에는 받는 쪽에서 도착을 확인해야 합니다.
🙋 학생 질문 — "튜터님, 개발할 때 난 오류까지 다 보내면 목록이 지저분해지지 않나요?"
맞습니다. 그래서 실무에서는 대개 갈라둡니다.
방법이 두 갈래예요. 하나는 environment 값으로 표시만 해두고 받는 쪽에서 걸러 보는 것인데, 우리 설정에 이미 들어 있는 그 줄입니다. 개발 중에 난 것과 배포본에서 난 것을 나중에 구분할 수 있게요.
다른 하나는 아예 안 보내는 것인데, 개발 중에는 dsn을 비워두면 도구가 조용히 아무 일도 안 해요.
오늘 우리는 첫 번째로 갑니다. 안 보내면 붙였는지 확인할 방법이 없어서 수업이 안 되는데, 사실 이건 실무에서도 초기 설정 때 겪는 딜레마예요. 개발 중에 안 보내면 배포하고 나서야 배선이 틀렸다는 걸 알게 됩니다.
배포본에서만 보내도록 바꾸는 건 오늘 과제로 다룹니다.
Step 3: "봉투를 열어봅니다"
받은 것을 다시 꺼내봅니다
연습용 서비스는 받은 걸 그대로 들고 있습니다. 브라우저로 열어보세요.
http://localhost:9000/envelopes
방금 보낸 오류가 통째로 들어 있습니다. raw 항목을 보면 브라우저가 실제로 보낸 글자 그대로예요.
JSON이 아닙니다
먼저 눈에 띄는 게 있습니다. 내용이 JSON 하나가 아니에요. 줄바꿈으로 나뉜 JSON이 여러 개 이어져 있습니다.
봉투 한 장 = 줄바꿈으로 나뉜 JSON 세 덩어리
1번째 줄 {"event_id":"0d83ac78…","sent_at":"…","sdk":{…},"trace":{…}}
2번째 줄 {"type":"event"}
3번째 줄 {"exception":{"values":[{"type":"Error","value":"…"}]}}
위에서부터 봉투 겉면 · 안에 든 것의 종류 · 실제 내용입니다. 편지 봉투랑 똑같아요. 겉에 받는 사람과 보낸 날짜를 적고, 안에 뭐가 들었는지 표시하고, 내용물을 넣죠.
왜 이렇게 만들었을까요. 한 번에 여러 개를 같이 보낼 수 있게 하려고요. 종류 표시와 내용을 짝지어 계속 이어 붙이면 오류 세 건을 봉투 한 장에 담을 수 있습니다. 요청을 세 번 보내는 것보다 낫죠.
무엇이 담겨 나가나
내용 쪽을 펼쳐서 읽어보세요. 우리가 적어준 적 없는 것들이 잔뜩 들어 있습니다.
- 오류 종류와 메시지
- 스택 트레이스 — 어느 함수가 어느 함수를 부르다 터졌는지
- 브라우저와 운영체제 종류
- 화면 주소
- 오류가 난 시각
Step 1의 🙋 에서 이야기한 그 빠진 정보들이죠. "며칠 전에 안 됐어요" 대신 이만큼이 자동으로 옵니다.
오류가 아닌 것도 나갑니다
연습용 서비스 터미널을 다시 보세요. 오류를 보내기 전에 이미 뭔가 한 건 와 있었을 겁니다.
[오류받음] 497 B · (id 없음)
session
session이라고 되어 있죠. 오류가 아닙니다. 누군가 앱을 열었다는 사실을 알리는 봉투예요. 도구를 켜는 순간 자동으로 한 번 나갑니다.
이걸로 받는 쪽은 "오류 30건"이라는 숫자를 "방문 10,000건 중 30건"으로 바꿔 읽을 수 있습니다. 같은 30건이라도 방문이 100건일 때와 10,000건일 때는 전혀 다른 이야기니까요.
⚠️ 그리고 이건 다르게 읽을 수도 있습니다. 우리가 명시적으로 시키지 않은 요청이 나가고 있다는 뜻이거든요. 도구를 앱에 들일 때는 그것이 무엇을 보내는지 한 번은 확인해야 합니다. 오늘처럼 받는 쪽을 직접 열어보면 확실하죠.
💡 한 줄 정리
봉투는 JSON 하나가 아니라 줄바꿈으로 이어 붙인 여러 개이고, 겉면·종류·내용 순서로 담깁니다. 안에는 우리가 적어준 적 없는 브라우저·주소·스택이 자동으로 들어가고, 오류가 아닌 것도 함께 나갑니다.
🙋 학생 질문 — "튜터님, 사용자 정보가 이렇게 나가도 괜찮은 건가요?"
좋은 질문이고, 실무에서 실제로 문제가 됩니다.
지금 나가는 것 중에 브라우저 종류나 화면 주소는 대체로 괜찮지만, 주소에 값이 섞여 있으면 이야기가 달라져요. 예를 들어 검색 화면에서 터졌는데 주소가 /search?q=병원+예약 이었다면, 그 사람이 무엇을 찾고 있었는지가 통째로 나갑니다.
더 위험한 건 오류 메시지 자체인데, 서버가 보낸 메시지를 그대로 담으면 그 안에 이메일이나 주문 번호가 들어 있을 수 있어서 이런 도구들은 보내기 직전에 내용을 손볼 수 있게 열어둡니다. 특정 값을 지우거나, 아예 그 건을 안 보내거나요.
오늘은 여기까지만 짚고 넘어갑니다. 다만 무엇이 나가는지 모르는 채로 켜두지는 마세요. 방금 우리가 한 것처럼 한 번 열어보는 것으로 충분합니다.
Step 4: "정작 진짜 오류는 안 올라갑니다"
이제 진짜 오류를 내봅니다
여기까지 잘 됐으니 확인은 끝난 것 같습니다. 그렇죠?
한 번만 더 해봅시다. 이번엔 손으로 부르는 게 아니라 진짜로 터뜨려서요.
Step 1에서 했던 걸 그대로 반복합니다. 연습용 API 서버를 끄고 홈 화면을 여세요. 몇 초 기다리면 오류 화면이 뜹니다.
문제가 생겼어요
서버에 연결할 수 없어요
홈으로
떴습니다. 연습용 오류 서비스 터미널을 봅니다.
아무것도 안 왔습니다
[오류받음] 497 B · (id 없음)
session
session 한 건뿐입니다. 오류 봉투는 0건이에요.
http://localhost:9000/envelopes를 열어봐도 마찬가지입니다. 앱을 열었다는 기록만 있고 터졌다는 기록은 없습니다.
잠깐 정리해봅시다. 우리는 방금
- 도구를 붙였고
- 손으로 부른 오류는 도착하는 걸 확인했고
- 화면에는 오류가 떴는데
- 오류는 안 왔습니다
왜 안 올라갈까요
원인은 우리가 잘 만들어둔 것에 있습니다.
이 도구는 브라우저가 "처리 안 된 오류가 있다" 고 알릴 때 그것을 듣고 있습니다. 아무도 안 잡아서 맨 위까지 굴러 올라온 오류를 낚아채는 방식이지만, 우리 앱에서 그 오류는 맨 위까지 안 올라갑니다. C-2에서 라우터에 오류 화면을 달아뒀거든요.
// apps/web-spa/src/routes/routes.ts
{
path: '/',
Component: Layout,
// Component 와 짝을 이룬다. 아래에서 무엇이 던져지든 여기로 올라온다.
ErrorBoundary: RootErrorBoundary,
children: [
// ...
],
},
주석에 적어둔 그대로입니다. 아래에서 무엇이 던져지든 여기서 받아요. 라우터가 받아버리니까 브라우저까지 갈 일이 없고, 브라우저까지 안 가니까 귀를 대고 있던 도구도 못 듣습니다.
그림으로 보면 이렇습니다.
오류가 굴러가는 길
컴포넌트에서 터짐
↓
라우터의 ErrorBoundary 가 받음 ← 여기서 멈춘다
↓
(브라우저까지 안 감)
↓
오류 추적 도구 ← 아무것도 못 듣는다
잘 만든 것이 눈을 가렸습니다
여기가 오늘의 요점입니다.
C-2에서 오류 화면을 만든 건 잘한 일이었어요. 그게 없으면 사용자는 빈 화면을 봤을 겁니다. C-7에서 throwOnError를 켠 것도 잘한 일이었고요. 그게 없으면 읽기 실패가 화면 안에서 뭉개져서 "불러오는 중"이 영원히 돌았을 겁니다.
그런데 잘 잡는다는 것은 곧 아무도 모른다는 뜻이기도 합니다. 잡아서 정중하게 처리했으니 밖으로는 아무 신호도 안 나가요.
⚠️ 그리고 이 상태를 알아차리기가 정말 어렵습니다. 빌드도 통과하고, 타입 검사도 통과하고, 화면도 정상이고, 도구도 "붙었다" 고 나오고, 손으로 테스트해본 오류는 도착까지 했으니까요. 여기서 확인을 멈추면 아무것도 안 잡히는 앱을 잡히는 줄 알고 몇 달을 보내게 됩니다.
💡 한 줄 정리
오류 추적 도구는 아무도 안 잡은 오류를 낚아챕니다. 그런데 우리 앱은 오류를 잘 잡도록 만들어져 있어서, 도구를 켜기만 해서는 화면에 오류가 떠도 봉투가 한 장도 안 나갑니다.
🙋 학생 질문 — "튜터님, 그럼 오류 화면을 떼면 되는 것 아닌가요?"
오류는 올라가겠지만, 대신 사용자가 흰 화면을 보게 됩니다.
React는 그리다가 터진 컴포넌트를 통째로 화면에서 걷어냅니다. 받아주는 경계가 없으면 그 위로 계속 올라가다가 결국 앱 전체가 사라져요. 우리가 C-2에서 오류 화면을 만든 이유가 바로 그거였습니다.
그러니까 이건 둘 중 하나를 고르는 문제가 아닙니다. 둘 다 해야 하는 일이에요. 사용자에게는 정중한 화면을 보여주고, 우리에게는 무슨 일이 있었는지 알려주고요.
지금 우리는 앞엣것만 하고 있습니다. 뒤엣것을 붙이는 게 다음 단계입니다.
Step 5: "경계를 이어줍니다"
React가 알려줍니다
방법은 생각보다 간단합니다.
React는 "경계가 오류를 잡았다" 고 알려줍니다. 앱을 그리기 시작할 때 함수를 하나 건네주면, 경계가 뭔가를 잡을 때마다 그 함수를 불러줍니다.
우리가 쓰는 도구는 거기에 딱 맞는 함수를 이미 만들어서 내보내고 있습니다. 이어주기만 하면 돼요.
// apps/web-spa/src/main.tsx
import * as Sentry from '@sentry/react';
// 라우터가 오류를 잡으면 화면은 멀쩡히 바뀌지만 밖으로는 아무것도 안 나간다.
// React 가 "경계가 잡았다" 고 알려주는 이 자리에서 넘겨줘야 우리가 알게 된다.
createRoot(rootElement, {
onCaughtError: Sentry.reactErrorHandler(),
onUncaughtError: Sentry.reactErrorHandler(),
}).render(
두 줄입니다.
위엣것은 경계가 잡은 오류입니다. 우리 앱의 오류 화면이 받아낸 것들이죠. 아까 안 올라가던 그 오류가 여기로 옵니다.
아래엣것은 아무도 못 잡은 오류예요. 경계 바깥에서 터져서 앱이 통째로 사라지는 경우입니다. 이건 원래 브라우저까지 굴러가니까 도구가 들을 수 있는데, 그래도 여기서 받아두면 React가 아는 추가 정보가 함께 실립니다.
다시 터뜨려봅니다
연습용 API 서버를 끈 채로 홈 화면을 새로고침하세요. 오류 화면이 뜨는 것까지는 아까와 똑같은데, 이번에는 연습용 오류 서비스 터미널을 봅니다.
[오류받음] … · 2af38c3f477743c4b77b8077bec68104
React ErrorBoundary Error: 서버에 연결할 수 없어요 ← …
왔습니다.
오류 종류를 보세요. 그냥 Error가 아니라 React ErrorBoundary Error입니다. 방금 이어준 그곳을 거쳐 왔다는 표시를 도구가 이름에 남긴 거예요. 손으로 부른 오류와 경계가 잡아준 오류를 나중에 목록에서 갈라 볼 수 있는데, 메시지는 C-6에서 우리가 만든 그 우리말 안내 문구가 그대로 실려 왔어요. 그때 영어 Network Error가 화면에 나가면 안 된다고 우리말로 갈아치웠던 그 문장입니다. 사용자에게 보여주려고 만든 문장이 이제 우리에게도 옵니다.
화면은 글자 하나 안 다릅니다
여기서 꼭 짚고 갈 게 있습니다.
두 줄을 넣기 전과 후, 사용자가 보는 화면은 완전히 같습니다. 글자 하나 안 달라요. 같은 "문제가 생겼어요"가 같은 위치에 같은 크기로 뜹니다.
두 줄을 넣기 전
화면에 뜬 것 문제가 생겼어요
나간 봉투 0건
두 줄을 넣은 뒤
화면에 뜬 것 문제가 생겼어요
나간 봉투 1건
눈으로는 절대 못 가릅니다. 화면을 아무리 들여다봐도, 우리가 오류를 알게 되는지 아닌지는 안 보여요.
⚠️ 그래서 이런 배선은 누가 실수로 지워도 아무도 모릅니다. 나중에 누군가 main.tsx를 정리하다가 이 두 줄을 걷어내면, 앱은 멀쩡히 돌아가고 화면도 그대로인 채로 우리 눈만 다시 감깁니다. 이 두 줄이 살아 있는지를 확인하는 장치가 코드베이스에 함께 들어 있는 이유예요.
이제 무엇이 잡히나
정리해봅시다. 오늘 우리가 이어둔 것으로 잡히는 건 그리는 도중에 터진 오류입니다.
컴포넌트가 그려지다 터진 것, 그리고 C-7에서 throwOnError로 위로 던지게 해둔 읽기 실패가 여기 들어갑니다. 읽기 실패는 화면을 그리는 중에 던져지거든요. 방금 확인한 게 정확히 그 경우입니다.
그럼 그리는 중이 아닐 때 터진 것은요?
그건 아직 안 잡힙니다. 다음 시간에 그 이야기부터 시작할게요.
💡 한 줄 정리
React가 "경계가 잡았다" 고 알릴 때 도구가 받도록 이어주면, 그때부터 오류 화면이 뜰 때마다 봉투가 나갑니다. 화면은 이어주기 전과 글자 하나 안 다르고, 갈리는 것은 우리가 그 일을 아느냐뿐입니다.
🙋 학생 질문 — "튜터님, 오류 화면 컴포넌트 안에서 직접 보내면 안 되나요?"
됩니다. 그리고 사실 그 방법이 필요한 경우가 곧 나옵니다.
단, 오늘 이 방법을 고른 이유가 있어요. 오류 화면은 앞으로 늘어날 수 있습니다. C-2에서 우리는 화면마다 다른 오류 화면을 달 수 있다는 걸 봤죠. 지금은 하나지만 나중에 프로필 전용, 쪽지 전용이 생길 수 있어요.
그때 각 오류 화면 안에서 보내게 만들어두면 새 오류 화면을 만드는 사람이 그 줄을 빠뜨릴 수 있고, 그러면 그 화면에서 난 오류만 조용히 사라져요. 어느 화면이 눈멀었는지 아무도 모르는 채로요.
앱을 그리기 시작할 때 한 번 걸어두면 아래에 무엇이 생기든 자동으로 걸립니다. 빠뜨릴 일이 없는 쪽을 고른 겁니다.
마무리
오늘 배운 핵심 세 가지
💡 하나. 프론트엔드 코드는 남의 기계에서 돕니다. 백엔드는 터지면 우리 서버 로그에 남지만, 브라우저에서 터진 것은 그 사람이 탭을 닫는 순간 사라집니다. 신고를 기다리는 것은 대책이 아니에요. 대부분은 조용히 떠나고, 오는 신고도 우리가 고칠 만한 정보를 안 담고 있습니다.
💡 둘. 도구를 붙였다는 것과 오류가 도착한다는 것은 다릅니다. DSN을 잘못 적어도 앱은 멀쩡하고, 보냈는지 확인하는 함수마저 참을 돌려줍니다. 확인은 언제나 받는 쪽에서 해야 합니다.
💡 셋. 잘 잡는다는 것은 아무도 모른다는 뜻이기도 합니다. 우리가 공들여 만든 오류 화면이 오류를 브라우저까지 못 가게 막고 있었고, 그래서 도구는 아무것도 못 들었습니다. 잡아서 정중히 보여주는 일과 그 일이 있었다고 알리는 일은 따로 이어줘야 합니다.
다음 시간 예고
오늘 마지막에 한 줄 흘려뒀습니다. 그리는 도중이 아닐 때 터진 오류는 아직 안 잡힌다고요.
다음 시간은 거기서 시작합니다. 오늘 이어둔 두 줄로 다 됐다고 생각하기 쉬운데, 실제로 재보면 우리 앱의 오류 중 상당수가 여전히 안 나가는데, 그 오류들은 화면이 오늘 본 것과 사실상 똑같이 생겨서 화면만 봐서는 어느 쪽이 도착하고 어느 쪽이 사라지는지 구분이 안 됩니다.
그것부터 메우고 나면 다음 문제가 기다립니다. 오늘 도착한 봉투의 스택 트레이스를 배포본에서 열어보면 읽을 수가 없어요. 함수 이름이 한 글자로 뭉개져 있고 줄 번호는 수만 번째를 가리킵니다. 오류가 도착해도 어디서 났는지 모르면 소용이 없죠.
그 두 가지를 다음 시간에 해결합니다.
과제
[구현] 개발 중에는 안 보내게 갈라보세요
Step 2의 🙋 에서 이야기했던 걸 직접 해보는 과제입니다.
지금은 개발 중에 난 오류도 전부 나갑니다. 이걸 배포본에서만 보내도록 바꿔보세요.
바꾼 뒤에 두 가지를 확인해야 합니다. 개발 서버에서는 오류를 내도 연습용 서비스에 아무것도 안 오는 것, 그리고 빌드해서 띄운 화면에서는 여전히 오는 것이요.
⚠️ 두 번째 확인을 빠뜨리기 쉽습니다. 안 보내게 만드는 건 쉬운데, 그러다 양쪽 다 안 보내게 만들어놓고 첫 번째 확인만 하고 넘어가는 실수가 흔한데, 그러면 배포본이 조용히 눈먼 상태가 됩니다.
다 확인했으면 되돌리세요. 남은 수업은 개발 서버에서 계속 확인해야 하니까요.
[구현] 누가 겪었는지 함께 보내보세요
지금 봉투에는 "무엇이 터졌나"는 있는데 "누가 겪었나"가 없습니다. 로그인한 사람이 겪은 오류라면 그 사람이 누구인지 함께 보내보세요.
이 도구에는 사용자 정보를 붙여두는 함수가 있습니다. 한 번 붙여두면 그 뒤로 나가는 봉투마다 자동으로 실립니다.
붙인 뒤 http://localhost:9000/envelopes에서 실제로 실려 나가는지 확인하세요.
그리고 답해보세요. 로그아웃하면 이 값은 어떻게 돼야 할까요? 그대로 두면 무슨 일이 생기나요?
[탐구] 어느 것이 도착하고 어느 것이 사라지는지 세어보세요
앱에서 오류를 낼 수 있는 방법을 최대한 여러 가지로 만들어보세요. 연습용 서버를 끄고 여러 화면을 열어보는 것도 좋고, 코드를 잠깐 고쳐 일부러 터뜨려도 좋습니다.
각각에 대해 두 가지를 기록하세요. 화면에 무엇이 떴는지, 그리고 봉투가 왔는지요.
| 무엇을 했나 | 화면 | 봉투 |
|---|---|---|
| 연습용 서버를 끄고 홈을 연다 | ||
표가 어느 정도 차면 규칙이 보일 겁니다. 도착하는 것과 사라지는 것을 가르는 기준이 무엇인지 여러분 말로 적어보세요.
⚠️ 결과가 예상과 다를 수 있습니다. 그럴 때 "내가 뭘 잘못했나"로 넘어가지 말고, 그 어긋남 자체를 기록해두세요. 다음 시간의 출발점입니다.
[탐구]이 도구가 얼마나 무거운지 재보세요
C-8에서 우리는 번들 크기를 재는 법을 익혔습니다. 같은 방법으로 오늘 들인 도구의 무게를 재보세요.
도구를 넣기 전과 후의 빌드 결과를 비교하면 됩니다. 넣기 전 크기는 지난 회차 기록을 쓰거나, 잠깐 걷어내고 빌드해서 직접 재도 되는데, CSS 파일 크기도 함께 보고 왜 그쪽은 안 변하는지 설명할 수 있나요?
마지막으로 판단해보세요. 이만큼을 사용자에게 내려보낼 값어치가 있을까요? 어떤 앱에서는 있고 어떤 앱에서는 없을 텐데, 그 기준이 무엇일지 적어보세요.
생각해볼 주제
1. 오류가 하나도 안 오는 앱은 어떤 상태일까요
오늘 우리는 봉투가 0건인 상황을 두 번 만났습니다. 한 번은 도구를 안 붙였을 때, 다른 한 번은 붙였는데 경계가 막고 있을 때요. 둘 다 화면은 멀쩡했습니다.
배포한 앱에서 일주일 동안 오류가 한 건도 안 왔다고 해봅시다. 이걸 어떻게 읽어야 할까요. 축하할 일인지, 의심할 일인지요.
그 둘을 어떻게 구분할 수 있을지도 생각해보세요. 잘 돌아가는 것과 눈이 멀어 있는 것이 밖에서 보기에 똑같다면, 우리는 무엇을 근거로 갈라야 할까요.
2. 모든 오류가 알림을 받을 값어치가 있을까요
오늘 우리는 오류가 나면 무조건 보내게 만들었습니다. 앱이 커지면 이게 문제가 됩니다.
사용자가 만 명이고 그중 백 명이 같은 오류를 겪었다면 봉투는 백 장이 오는데, 고칠 것은 하나죠. 반대로 딱 한 사람만 겪었는데 그게 결제가 안 되는 오류라면, 백 장짜리보다 급합니다.
무엇을 기준으로 급한 것과 안 급한 것을 갈라야 할지 생각해보세요. 겪은 사람 수일까요, 그 화면이 하는 일일까요, 아니면 다른 무엇일까요.
알림이 너무 많이 오면 어떻게 되는지도 함께 생각해보세요. 사람이 알림을 무시하기 시작하는 순간, 그 장치는 없는 것과 무엇이 다를까요.
3. 우리가 안 만든 코드에서 터진 오류는 누구 몫일까요
오늘 봉투에 실린 스택 트레이스를 보면 우리가 쓴 코드만 있는 게 아닙니다. 라이브러리 코드도 있고, 경우에 따라서는 사용자가 깔아둔 브라우저 확장 프로그램 코드도 섞여 들어옵니다.
실무에서 오류 목록을 열면 우리 코드와 전혀 상관없는 것들이 꽤 많이 쌓입니다. 광고 차단기가 우리 요청을 막아서 난 것, 번역 확장이 화면을 고쳐서 난 것 같은 것들이요.
이런 것들을 어떻게 다뤄야 할지 생각해보세요. 전부 무시해도 될까요? 무시한다면 그중에 진짜 우리 문제가 섞여 있을 가능성은 어떻게 다룰까요.
✅ 예시 답안정답 보기
과제
🎯 [과제 1 예시답안] 개발 중에는 안 보내게 갈라보세요
채점 포인트
| 항목 | 배점 | 확인할 것 |
|---|---|---|
| 개발 서버에서 안 나가게 만들었다 | 8 | 오류를 내도 연습용 서비스에 아무것도 안 찍힌다 |
| 배포본에서는 여전히 나가는 것까지 확인했다 | 10 | 빌드해서 띄운 화면에서 오류를 내고 도착을 봤다 |
| 확인을 받는 쪽에서 했다 | 4 | flush() 반환값이 아니라 연습용 서비스 화면으로 판정 |
| 확인 후 되돌렸다 | 3 | 남은 수업이 개발 서버에서 이어지므로 |
| 합계 | 25 |
풀이 예시
갈래를 나누는 방법은 여러 가지인데, 가장 간단한 건 DSN 자체를 비우는 것입니다.
// apps/web-spa/src/lib/monitoring.ts
const DSN = import.meta.env.VITE_SENTRY_DSN ?? 'http://demopublickey123@localhost:9000/7';
export function startMonitoring(): void {
Sentry.init({
// 개발 중에는 아예 안 보낸다. 값을 안 주면 도구가 조용히 아무 일도 안 한다.
dsn: import.meta.env.PROD ? DSN : undefined,
tracesSampleRate: 0,
environment: import.meta.env.DEV ? 'development' : 'production',
});
}
확인해보면 이렇습니다.
| 조건 | 나간 봉투 | flush() |
|---|---|---|
dsn을 안 줌 |
0건 | true |
dsn을 줌 |
1건 | true |
dsn을 안 줘도 앱은 아무 불평 없이 돌아가는데, 오류를 내도 콘솔에 경고 한 줄 안 나오고 flush()도 양쪽 다 true를 돌려줍니다. Step 2에서 하이픈을 넣었을 때와 똑같이, 보낼 게 없으면 "다 보냈다"가 참이니까요.
배포본 확인은 이렇게 합니다.
npm run build
npm run preview
띄운 화면에서 오류를 내고, 연습용 서비스 터미널에 찍히는지 보세요. 찍혀야 정답입니다.
자주 나오는 실수
절반만 확인하고 넘어가는 것. 이게 압도적으로 흔합니다.
개발 서버에서 "안 나간다"를 확인하면 성공한 것 같지만, 조건을 잘못 쓰면 양쪽 다 안 나갑니다. 예를 들어 import.meta.env.PROD 대신 없는 이름을 쓰면 그 값은 undefined라 언제나 거짓이 되고, 배포본도 조용히 눈이 멉니다.
이 실수는 배포하고 나서도 안 드러나는데, 오류가 안 오는 게 "앱이 안정적이라서"인지 "우리가 눈을 감아서"인지 밖에서는 똑같이 생겼기 때문입니다. 생각해볼 주제 1이 바로 이 이야기입니다.
💡 튜터의 한마디
이 과제의 진짜 요점은 갈래를 나누는 문법이 아니라 확인을 두 번 해야 한다는 겁니다.
무언가를 "안 하게" 만드는 변경은 확인이 쉬워 보여서 한쪽만 보고 끝내기 쉬운데, 안 하게 만드는 변경일수록 너무 많이 안 하게 될 위험이 큽니다. 껐다는 걸 확인했으면, 켜져 있어야 하는 쪽도 반드시 함께 확인하세요.
🎯 [과제 2 예시답안] 누가 겪었는지 함께 보내보세요
채점 포인트
| 항목 | 배점 | 확인할 것 |
|---|---|---|
| 로그인한 사람 정보를 붙였다 | 7 | 봉투에 "user"가 실려 나간다 |
| 봉투에서 직접 확인했다 | 5 | /envelopes를 열어 눈으로 봤다 |
| 로그아웃하면 어떻게 되는지 답했다 | 9 | 안 지우면 남는다는 것을 실행으로 확인 |
| 지우는 방법까지 확인했다 | 4 | 지운 뒤 봉투가 어떻게 달라지는지 |
| 합계 | 25 |
풀이 예시
이 도구에는 사람 정보를 붙여두는 함수가 있습니다. 한 번 부르면 그 뒤로 나가는 봉투마다 자동으로 실립니다.
C-7에서 만든 로그인·로그아웃 자리에 붙이면 됩니다.
import * as Sentry from '@sentry/react';
// 로그인에 성공했을 때
Sentry.setUser({ username });
// 로그아웃했을 때
Sentry.setUser(null);
붙이고 오류를 내면 봉투 안에 이렇게 들어갑니다.
"user":{"username":"jaehoon"}
로그아웃하면 어떻게 되나
안 지우면 그대로 남습니다. 이게 이 과제의 핵심입니다.
실행해서 확인해보면 이렇습니다.
| 무엇을 했나 | 봉투의 user |
|---|---|
setUser({ username: 'jaehoon' }) 후 오류 |
{"username":"jaehoon"} |
| 로그아웃했는데 안 지우고 오류 | {"username":"jaehoon"} ← 그대로 |
setUser(null) 후 오류 |
{} |
가운데 줄이 문제입니다. 재훈이가 로그아웃하고 나갔는데, 그 뒤에 그 브라우저에서 난 오류에 여전히 재훈이 이름이 붙어요.
여기서 두 가지가 동시에 망가집니다. 하나는 사실이 틀립니다. 재훈이가 안 겪은 일을 재훈이가 겪은 것으로 기록해요. 다른 하나는 개인정보 문제입니다. 공용 컴퓨터라면 다음 사람이 겪은 오류에 앞사람 이름이 실려 나갑니다.
자주 나오는 실수
로그인할 때만 붙이고 로그아웃 쪽을 빠뜨리는 것. 화면상으로는 아무 문제가 없어서 끝까지 안 드러납니다.
그리고 이건 Step 5에서 이야기한 것과 같은 종류예요. 빠뜨릴 수 있으면 언젠가 빠뜨립니다. 그래서 실무에서는 로그인 상태를 한곳에서 관리하고, 그 값이 바뀔 때 함께 갱신되도록 묶어둡니다.
💡 튜터의 한마디
Step 3의 🙋 에서 개인정보 이야기를 했었죠. 이 과제는 그 반대편입니다. 정보를 덜 보내는 게 아니라 더 보내는 쪽이요.
이 둘은 충돌하는 것 같지만 사실 같은 원칙입니다. 의도한 것만 보낸다. 필요한 건 붙이고, 필요 없어진 건 지우고요. 붙인 채로 잊는 것은 안 붙인 것만큼이나 문제입니다.
🎯 [과제 3 예시답안] 어느 것이 도착하고 어느 것이 사라지는지 세어보세요
채점 포인트
| 항목 | 배점 | 확인할 것 |
|---|---|---|
| 오류를 내는 방법을 세 가지 이상 만들었다 | 6 | 서버 끄기 외에 다른 것도 시도 |
| 화면과 봉투를 따로 기록했다 | 7 | 둘을 한 칸에 뭉치지 않았다 |
| 도착하지 않는 경우를 찾아냈다 | 8 | 이어뒀는데도 안 오는 것이 있다 |
| 가르는 기준을 자기 말로 적었다 | 4 | 맞고 틀리고보다 근거가 실측인지 |
| 합계 | 25 |
풀이 예시
표를 채우면 대략 이렇게 나옵니다.
| 무엇을 했나 | 화면 | 봉투 |
|---|---|---|
| 연습용 서버를 끄고 홈을 연다 | 문제가 생겼어요 · 서버에 연결할 수 없어요 | 온다 |
컴포넌트에서 일부러 throw |
문제가 생겼어요 · (그 메시지) | 온다 |
콘솔에서 captureException |
아무 변화 없음 | 온다 |
연습용 서버를 끄고 /p/999를 연다 |
문제가 생겼어요 · 서버에 연결할 수 없어요 | 안 온다 |
없는 주소 /zzzzz를 연다 |
없는 주소예요 | 안 온다 |
여기서 무엇이 보이나요
위에서 두 줄과 네 번째 줄을 나란히 놓아보세요.
화면이 거의 같습니다. 같은 "문제가 생겼어요"에 같은 "서버에 연결할 수 없어요"예요. 그런데 하나는 오고 하나는 안 옵니다.
이게 이 과제의 목적입니다. 화면만 봐서는 우리가 그 오류를 아는지 모르는지 알 수 없다는 것이요.
가르는 기준을 지금 정확히 말하기는 어려울 겁니다. 그래도 관찰만으로 여기까지는 갈 수 있어요. 주소를 열자마자 실패하는 것들이 안 오는 쪽에 몰려 있다는 것, 그리고 화면이 그려지다가 실패한 것들이 오는 쪽에 몰려 있다는 것이요.
거기까지 적었으면 만점입니다. 이유는 다음 시간에 다룹니다.
자주 나오는 실수
결과가 예상과 다를 때 자기 코드를 의심하는 것.
네 번째 줄에서 봉투가 안 오면 대부분 "내가 배선을 잘못했나" 하고 Step 5를 다시 확인합니다. 그런데 첫 줄은 잘 오고 있죠. 배선은 멀쩡합니다.
이럴 때 필요한 건 코드를 고치는 게 아니라 어긋남을 기록해두는 것입니다. 실무에서 진짜 어려운 문제는 대개 이렇게 생겼어요. 되는 경우와 안 되는 경우가 같이 있고, 둘의 차이가 눈에 안 보이는 형태로요.
💡 튜터의 한마디
마지막 줄(/zzzzz)은 조금 다른 이야기입니다. 이건 안 오는 게 맞아요.
없는 주소를 연 건 앱이 고장 난 게 아니라 사용자가 없는 주소를 연 겁니다. 애초에 오류가 나지도 않았어요. C-2에서 만든 대로 표 맨 끝의 *가 그 주소를 받아서 "없는 주소예요" 화면을 정상적으로 그린 겁니다. 우리가 고칠 것이 없죠. 이런 것까지 알림이 오면 진짜 문제가 그 사이에 묻힙니다.
그러니까 "안 오는 것" 안에도 두 종류가 있는 셈입니다. 와야 하는데 안 오는 것과 안 와야 해서 안 오는 것이요. 다음 시간에 이 둘을 갈라냅니다.
🎯 [과제 4 예시답안]이 도구가 얼마나 무거운지 재보세요
채점 포인트
| 항목 | 배점 | 확인할 것 |
|---|---|---|
| 넣기 전후를 같은 방법으로 쟀다 | 7 | 한쪽만 gzip 크기를 보는 식이 아님 |
| CSS 가 왜 안 변하는지 설명했다 | 8 | 스타일을 안 만드는 변경이라는 것 |
| 값어치 판단에 기준을 댔다 | 6 | "크다/작다" 가 아니라 무엇 대비인지 |
| 어떤 앱에서 갈리는지 말했다 | 4 | 조건을 하나 이상 제시 |
| 합계 | 25 |
풀이 예시
제 노트북에서 잰 값입니다. 여러분 숫자는 조금 다를 수 있어요.
| 넣기 전 | 넣은 뒤 | 차이 | |
|---|---|---|---|
| 자바스크립트 | 531,727 B | 617,660 B | +85,933 B |
| CSS | 43,981 B | 43,981 B | 0 |
자바스크립트가 약 86 KB 늘었습니다. gzip으로 줄여서 내려가는 걸 감안해도 적은 양은 아니에요.
CSS는 왜 안 변할까요
오늘 우리가 한 일에 화면에 보이는 것을 만든 게 하나도 없기 때문입니다.
E-1에서 배웠듯이 Tailwind는 우리 소스에서 클래스 이름 문자열을 찾아 그만큼만 CSS를 만듭니다. 오늘 우리는 클래스를 하나도 안 추가했어요. 오류가 나도 보여주는 화면은 C-2에서 이미 만들어둔 그것이고요.
C-3~C-6에서도 같은 일이 있었습니다. 상태 관리를 넣고 데이터 계층을 넣어도 CSS는 안 움직였죠. 어떤 계층을 건드리는 작업인지가 어느 파일이 커지는지로 드러납니다.
값어치가 있을까요
86 KB를 무엇과 견줄지가 관건입니다.
우리 앱의 자바스크립트 전체가 약 618 KB 이니 그중 14퍼센트쯤입니다. 게시물 사진 한 장이 보통 이보다 큽니다. 그렇게 보면 크지 않아요.
그런데 이건 모든 사용자가 내려받습니다. 오류를 안 겪는 아흔아홉 명도요. 그 사람들에게는 순수한 손해입니다.
갈리는 조건을 몇 개 들자면 이렇습니다.
- 사용자가 많고 화면이 복잡할수록 값어치가 큽니다. 우리가 못 본 조합에서 터질 확률이 높으니까요.
- 첫 화면 속도가 사업에 직결되는 앱이면 부담이 큽니다. 쇼핑몰 첫 화면 같은 곳이요.
- 사내 도구처럼 사용자가 정해져 있는 앱은 오히려 덜 필요할 수 있습니다. 문제가 생기면 옆자리에서 바로 말해주거든요.
자주 나오는 실수
넣기 전 크기를 지난 회차 기록에서 가져오면서 측정 조건이 달랐던 것을 안 따지는 것입니다.
같은 저장소라도 그사이에 다른 게 늘었을 수 있어요. 정확히 재려면 지금 시점에서 도구만 잠깐 걷어내고 한 번, 되돌리고 한 번 재는 게 맞습니다.
💡 튜터의 한마디
C-8에서 "측정 먼저"를 배웠죠. 그때는 성능 이야기였는데 오늘은 도입 판단에 같은 원칙을 씁니다.
도구를 들일 때 "좋다더라"로 결정하면 나중에 되돌리기가 어렵습니다. 재보고 들이면 나중에 누가 "이거 왜 있어요?" 물었을 때 답할 수 있어요. 그리고 그 답에는 숫자가 들어 있어야 합니다.
생각해볼 주제
🤔 [생각해볼 주제 1] 오류가 하나도 안 오는 앱은 어떤 상태일까요
문제 상황 요약
배포한 앱에서 일주일 동안 오류가 한 건도 안 왔습니다. 축하할 일일까요, 의심할 일일까요.
오늘 우리는 봉투가 0건인 상황을 두 번 만났습니다. 한 번은 도구를 안 붙였을 때, 다른 한 번은 붙였는데 경계가 막고 있을 때요. 둘 다 화면은 멀쩡했습니다.
튜터의 가이드 및 해설
결론부터 말하면 의심해야 합니다. 정확히는, 의심할 수 있는 상태로 만들어두지 않은 것이 문제입니다.
잘 돌아가는 앱과 눈이 먼 앱은 밖에서 보기에 완전히 같습니다. 오류 목록이 비어 있다는 사실 하나로는 둘을 못 가릅니다. 오늘 우리가 Step 4에서 정확히 그 상황에 있었죠. 도구도 붙였고 손으로 보낸 오류는 도착까지 했는데, 정작 진짜 오류는 하나도 안 올라오고 있었습니다.
그럼 어떻게 갈라야 할까요. 세 가지 방향이 있습니다.
하나. 오류가 아닌 신호를 함께 봅니다. Step 3에서 본 session 봉투가 이 역할을 합니다. 오류가 0건이어도 방문이 10,000건 찍히고 있으면 적어도 "연결은 살아 있다"는 건 압니다. 방문마저 0이면 배선이 끊긴 거예요.
둘. 일부러 하나 터뜨려봅니다. 배포할 때마다 오류를 하나 만들어 보내고, 그게 도착하는지 확인하는 방법입니다. 연기 감지기 시험 버튼과 같은 원리예요. 실무에서는 배포 절차에 이걸 넣어두기도 합니다.
셋. 침묵에 시한을 둡니다. "24시간 동안 아무 신호도 없으면 그것 자체를 알림으로" 만드는 겁니다. 없는 것을 감지하는 건 있는 것을 감지하는 것보다 어렵지만, 이 경우엔 그게 핵심이라서요.
이 문제는 오류 추적에만 있는 게 아닙니다. 무언가를 감지하는 장치는 전부 "장치 자체가 죽었을 때"를 함께 설계해야 합니다. 백업이 도는 줄 알았는데 6개월째 실패하고 있었다는 이야기가 업계에 흔한 것도 같은 이유예요.
🎯 면접관을 홀리는 핵심 멘트
"오류가 0건이라는 건 좋은 신호일 수도 있고 계측이 죽었다는 신호일 수도 있는데, 둘이 밖에서 보기엔 똑같습니다. 그래서 저는 오류 지표만 보지 않고 세션 같은 살아 있음 신호를 함께 봅니다. 감지 장치를 붙일 때는 그 장치가 죽었을 때 어떻게 알아챌지까지가 한 세트라고 생각합니다."
🤔 [생각해볼 주제 2] 모든 오류가 알림을 받을 값어치가 있을까요
문제 상황 요약
오늘 우리는 오류가 나면 무조건 보내게 만들었습니다. 사용자가 만 명이고 그중 백 명이 같은 오류를 겪었다면 봉투는 백 장이 오는데, 고칠 것은 하나죠.
반대로 딱 한 사람만 겪었는데 그게 결제가 안 되는 오류라면, 백 장짜리보다 급합니다.
튜터의 가이드 및 해설
먼저 보내는 일과 알리는 일을 갈라야 합니다. 이 둘을 붙여 생각하면 답이 안 나와요.
봉투는 다 보내는 게 맞습니다. 나중에 "이 오류가 언제부터 늘었나"를 보려면 숫자가 필요하니까요. 문제는 그중 무엇이 사람을 깨울 것이냐입니다.
기준으로 쓸 만한 축이 셋 있습니다.
하나. 몇 명이 겪었나. 가장 직관적이지만 이것만 보면 위 예시의 결제 오류를 놓칩니다. 그리고 사용자가 적은 새 기능은 영원히 기준에 못 닿아요.
둘. 어느 화면에서 났나. 결제·로그인처럼 못 넘어가면 끝인 곳과, 프로필 사진이 안 뜨는 정도를 같이 취급할 이유가 없습니다. 실무에서는 화면마다 등급을 미리 매겨둡니다.
셋. 새로 생긴 것인가. 전날까지 없던 오류가 오늘 나타났다면, 건수가 적어도 방금 배포한 것이 원인일 가능성이 높습니다. 변화율이 절대량보다 나은 신호일 때가 많아요.
그리고 알림이 너무 많으면 어떻게 되는지도 생각해봐야 합니다. 사람은 알림이 잦아지면 읽지 않고 지우기 시작합니다. 이렇게 되면 그 장치는 없는 것보다 나쁩니다. 없으면 없는 줄 알고 다른 대비를 하는데, 있는데 안 보면 대비했다고 착각하거든요.
그래서 알림을 설계할 때 가장 중요한 질문은 "무엇을 알릴까"가 아니라 "이 알림을 받은 사람이 지금 당장 할 일이 있는가"입니다. 할 일이 없는 알림은 알림이 아니라 소음이에요.
🎯 면접관을 홀리는 핵심 멘트
"기록은 전부 남기되 사람을 깨우는 건 아껴야 한다고 봅니다. 알림이 잦아지면 사람이 읽지 않고 지우기 시작하는데, 그 순간부터는 장치가 없는 것보다 나빠집니다. 대비했다고 착각하게 되니까요. 그래서 저는 알림 조건을 정할 때 건수보다 '받은 사람이 지금 할 일이 있는가'를 먼저 봅니다."
🤔 [생각해볼 주제 3] 우리가 안 만든 코드에서 터진 오류는 누구 몫일까요
문제 상황 요약
봉투에 실린 스택 트레이스에는 우리가 쓴 코드만 있는 게 아닙니다. 라이브러리 코드도 있고, 사용자가 깔아둔 브라우저 확장 프로그램 코드도 섞여 들어옵니다.
실무에서 오류 목록을 열면 우리와 상관없어 보이는 것들이 꽤 쌓입니다. 광고 차단기가 우리 요청을 막아서 난 것, 번역 확장이 화면을 고쳐서 난 것 같은 것들이요.
튜터의 가이드 및 해설
전부 무시하는 것도, 전부 파는 것도 답이 아닙니다. 갈라야 합니다.
먼저 분명한 것부터. 확장 프로그램이 자기 코드 안에서 터진 것은 우리 몫이 아닙니다. 스택에 우리 파일이 한 줄도 안 나오면 우리가 고칠 것이 없어요. 이런 건 걸러서 안 보이게 하는 게 맞습니다.
문제는 섞인 경우입니다. 번역 확장이 우리 화면의 글자를 바꿔놓아서 우리 코드가 터진 상황을 생각해보세요. 스택에는 우리 파일이 나옵니다. 그리고 실제로 우리 사용자가 화면을 못 쓰고 있어요.
이걸 "확장 프로그램 탓"으로 넘기면 그 사용자는 그냥 못 쓰는 채로 남습니다. 그 사람 입장에서는 우리 앱이 고장 난 거고요. 우리가 원인 제공자가 아니라는 것과 우리가 손쓸 수 없다는 것은 다른 이야기입니다.
그래서 판단 기준을 이렇게 잡는 게 좋습니다. "누구 잘못인가"가 아니라 "우리가 고칠 수 있는가"로요. 앞엣것은 답이 나와도 아무것도 안 바뀌지만, 뒤엣것은 할 일을 알려줍니다.
여기에 하나 더. C-3에서 봤듯이 걸러내는 규칙은 한 번 만들면 조용히 계속 일합니다. "확장 프로그램 관련은 다 무시" 같은 규칙을 넓게 걸어두면, 나중에 진짜 우리 문제가 그 그물에 걸려 사라져도 아무도 모릅니다. 걸러내는 규칙일수록 좁게 걸고, 왜 걸었는지 적어두는 편이 안전합니다.
🎯 면접관을 홀리는 핵심 멘트
"저는 외부 요인으로 보이는 오류를 '누구 잘못인가'로 판단하지 않고 '우리가 고칠 수 있는가'로 나눕니다. 원인이 확장 프로그램이어도 사용자는 우리 앱이 안 된다고 느끼니까요. 그리고 걸러내는 규칙은 넓게 걸면 나중에 진짜 문제까지 조용히 삼키기 때문에, 좁게 걸고 이유를 남기는 편입니다."