안녕하세요! 그로스로그 입니다!😊 🌱
내가 직접 할 일과 남에게 맡길 일. 그 선을 어디에 그을지 각자 다른 방식으로 답한 세 편입니다.
성장일지는 멤버들이 2주에 한 번씩 자신의 성장을 기록하는 활동이에요. 매 회차 운영진이 모든 글을 읽고, 그중 특히 마음에 남은 글을 함께 골라 큐레이션합니다.
3회차는 밀도가 높습니다. 스펙 문서 정독기, 실제 장애 부검 리포트, 그리고 스터디 운영 자동화. 셋 다 길고 구체적이에요.
01
🔌 웹소켓 클라이언트를 다시 구현해야 한다면
오세명 님
웹소켓 클라이언트를 처음부터 다시 만든다면 무엇을 챙길 것인가. 실무 경험을 스펙 문서와 대조해가며 정리한 글입니다.
세 덩어리예요. Ping/Pong — 살아 있는지 확인하는 메커니즘. Closing — 어떻게 끝내야 하는가. 재연결 — 언제 다시 붙어야 하는가.
인상적인 건 근거를 전부 명시한다는 점입니다. RFC 프로토콜 문서와 WHATWG 스펙을 직접 인용하고, 각주 네 개를 달았어요.
3회차 연속으로 소개하게 된 저자입니다. 1회차는 코딩 습관, 2회차는 6시간짜리 만들기, 이번엔 스펙 정독기예요. 결이 매번 다릅니다.
조금 더 들어가 보면
이 글의 첫 문장에 이미 태도가 들어 있습니다.
"구현하기 전에 왜 구현하는지부터"입니다. 그리고 이어지는 조사 결과가 통념을 뒤집어요.
🏓 Ping/Pong — 이미 브라우저가 하고 있다
웹소켓 연결이 끊겼는지 확인하려고 하트비트를 직접 만드는 코드는 아주 흔합니다. 몇 초마다 "ping"이라는 문자열을 보내고 서버가 "pong"을 돌려주는 식이죠.
그런데 글은 프로토콜에 이미 그게 있다고 짚습니다.
0x9)을 보내면 다른 한쪽이 반드시 Pong 프레임(opcode가 0xA)을 보내야 한다고 규정하였다."여기서 프레임의 opcode란 "이 데이터가 무슨 종류인지"를 나타내는 표시입니다. 텍스트인지, 바이너리인지, 종료 신호인지, 핑인지. 프로토콜 차원에서 핑퐁이 정의돼 있는 거예요.
그런데 다음 문장이 중요합니다.
브라우저의 WebSocket 객체에는 핑을 보내는 메서드가 없습니다. 일부러 안 만들었어요. 스펙이 그렇게 규정했습니다.
왜냐하면 브라우저가 알아서 하고 있기 때문입니다. 서버가 Ping 프레임을 보내면 브라우저가 Pong으로 답합니다. 우리 코드가 관여하지 않는 곳에서요.
그러니까 하트비트를 직접 만드는 코드는 이미 있는 걸 한 층 위에서 또 만드는 셈입니다. 그리고 이건 프로토콜의 진짜 핑퐁이 아니라, 그냥 평범한 텍스트 메시지예요. 서버가 그걸 알아듣게 따로 짜줘야 합니다.
🧭 그런데 왜 실무에서는 여전히 만드는가
글이 짚은 지점이 흥미롭습니다.
핑을 먼저 보내는 쪽이 서버로 상정돼 있습니다. 그래서 서버가 안 보내면 아무 일도 안 일어나요. 그리고 서버 라이브러리들은 이 설정이 기본으로 꺼져 있거나 옵션인 경우가 많습니다.
여기서 실무 조언이 나옵니다.
아주 자주 겪는 증상입니다. 개발 환경에서는 멀쩡한데 배포하면 1분쯤 뒤 연결이 끊기는 현상이요. 원인은 대개 앞단의 프록시입니다. 아무 데이터도 안 흐르면 죽은 연결로 보고 끊어버리거든요. 흔히 쓰는 웹서버들의 기본 유휴 타임아웃이 60초 안팎입니다.
그래서 글의 처방이 정확해요. "적어도 Ping 프레임을 보내는 인터벌보다는 길게 timeout 설정이 되어 있어야." 즉 핑 간격 < 프록시 타임아웃이어야 합니다.
⏰ 타이머 쓰로틀링이라는 함정
그리고 마지막 경고가 실전적입니다.
이게 왜 무서운 함정이냐면요.
브라우저는 배터리를 아끼려고 백그라운드 탭의 타이머를 느리게 만듭니다. 탭이 보이지 않으면 setInterval(fn, 5000)이 5초마다 안 돌아요. 1분에 한 번으로 늘어질 수 있습니다.
그러면 어떤 일이 생기냐면 — 탭을 다른 데 두고 있으면 하트비트가 멈춥니다. 프록시는 유휴로 판단하고 끊고요. 사용자가 탭으로 돌아왔을 때 연결이 죽어 있습니다.
하트비트를 직접 만들었기 때문에 생긴 문제입니다. 브라우저가 관리하는 프로토콜 핑퐁은 자바스크립트 타이머와 무관하니까 이 영향을 안 받아요.
🚪 Closing — 문을 제대로 닫는다는 것
두 번째 덩어리는 종료입니다.
Document 객체가 정리되는 시점(Goes away)에 웹소켓 연결이 되어있으나 아직 종료하지 않았을 때에는 1001 코드와 함께 종료 절차를 진행하도록 규정한다."그리고 그 경우를 나열합니다.
· 사용자가 새로고침을 하는 경우
· 사용자가 탭을 닫는 경우
· 사용자가 브라우저를 닫는 경우
· 프로젝트가 MPA이고 사용자가 다른 페이지로 이동하는 경우
· Document가 discarded 되는 경우
"이런 케이스들에 대해서는 유저 에이전트에서 코드를 보내도록 하는게 가장 깔끔할 것이다!"
또 브라우저에 맡기라는 결론입니다. beforeunload에 socket.close()를 붙이는 코드를 종종 보는데, 위 다섯 가지를 전부 잡아내기가 어렵고 브라우저가 이미 처리하고 있어요.
📝 원문에서는 이렇게 예외를 다룹니다
다만 예외가 하나 있습니다. bfcache입니다.
function onPageHide(event: PageTransitionEvent) {
if (event.persisted) {
// closing..
}
}
function onPageShow(event: PageTransitionEvent) {
if (event.persisted) {
// reconnecting..
}
}
bfcache는 뒤로 가기를 빠르게 만드는 브라우저 기능입니다. 페이지를 버리지 않고 통째로 얼려뒀다가, 돌아오면 그대로 되살려요. 그래서 스크롤 위치도 입력값도 남아 있습니다.
문제는 웹소켓도 같이 얼어붙는다는 겁니다. 얼어 있는 동안 서버는 응답 없는 연결로 보고 끊고, 되살아났을 때 클라이언트만 "연결됐다"고 착각하는 상태가 됩니다.
그래서 얼기 직전(pagehide)에 닫고, 깨어난 뒤(pageshow)에 다시 붙는 처리가 필요해요. event.persisted가 bfcache로 들어가는/나오는 경우인지 구분해주는 값입니다.
🔁 재연결 — 세 가지 경우로 나누기
마지막 덩어리가 가장 실용적입니다. 재연결 판단을 세 갈래로 나눠요.
첫째, 깨끗하지 않게 닫힌 경우. CloseEvent의 wasClean이 false고, 코드는 대개 1006입니다.
각주에 붙은 설명이 이 글에서 가장 흥미로운 대목이었습니다.
reason에 왜 1006이 전달되었는지 구체적인 맥락을 전달하지 않는다. 즉 이 코드를 받았을 때 왜 받았는지를 유저랜드에서 알 수 없다. 이 이유를 노출하면 로컬 네트워크 구조를 탐지할 수 있는 보안 취약점을 만들 수 있기 때문이다.""왜 끊겼는지 알 수 없다"가 버그가 아니라 의도된 설계라는 겁니다.
이유를 알려주면 어떻게 되냐면, 악의적인 페이지가 여러 주소로 연결을 시도해보면서 에러 메시지 차이로 내부망 구조를 알아낼 수 있습니다. "이 주소는 연결 거부, 저 주소는 타임아웃" 같은 차이가 지도가 되거든요. 그래서 브라우저가 전부 똑같이 1006으로 뭉뚱그립니다.
디버깅할 때 답답한 이 동작에 이유가 있다는 걸 알아낸 게 이 글의 성실함입니다.
둘째, 깨끗하게 닫혔지만 다시 붙어야 하는 경우. wasClean은 true인데 code와 reason이 재연결을 요구하는 상황이죠. "이건 팀마다 정한 규칙에 따라"라고 정직하게 남겨둡니다. 정답이 없는 영역이니까요.
셋째, 처음부터 연결이 실패한 경우. 재연결 로직은 보통 "붙었다가 끊긴" 경우만 생각하는데, 처음부터 못 붙은 경우도 챙기라고 짚습니다. 놓치기 쉬운 지점이에요.
큐레이터 노트
스펙을 근거로 삼은 글이라 뽑았습니다.
웹소켓 관련 글은 대부분 "이렇게 하면 됩니다" 형태의 코드 모음입니다. 이 글은 "스펙이 이렇게 규정했다"로 말해요. RFC 프로토콜 문서와 WHATWG 스펙을 각각 인용하고, 둘이 어떻게 다른지도 구분합니다.
그래서 결론이 남과 다릅니다. 대부분의 글은 "하트비트를 구현합시다"라고 하는데, 이 글은 "이미 브라우저가 하고 있으니 굳이 하지 마라"고 해요. 그리고 그 근거를 스펙 문장으로 댑니다.
각주 문화도 좋았습니다. 본문 흐름을 끊지 않으면서 깊은 이야기를 따로 담아뒀어요. 1006의 보안 배경 같은 건 각주였기에 들어갈 수 있었던 내용입니다.
제목도 정확합니다. "다시 구현해야 한다면" — 이미 한 번 해본 사람이 쓴 글이라는 뜻이에요. 처음 배우며 쓴 정리와 한 바퀴 돌고 나서 쓴 정리는 다릅니다. 이 글은 후자예요.
02
🔥 6/26 DB Connection Pool 고갈 장애 분석 및 해결 방안 리포트
이준호 님
실제로 서비스가 멈춘 날의 부검 리포트입니다. 오전 9시 14분 최초 보고, 하루 동안 멤버십 가입과 취소·교환·반품이 안 됐어요.
에러는 한 줄이었습니다.
Knex: Timeout acquiring a connection. The pool is probably full그런데 원인은 네 가지가 겹친 결과였습니다. 루프 안의
flush, em.fork() 남용, 롤백 누락, 동시성 제어 없는 웹훅. 각각을 코드와 함께 짚고 단기·장기 대응을 나눠 제시합니다.블로그 글이라기보다 사내 문서에 가깝습니다. 그래서 더 귀합니다. 이런 문서는 보통 밖으로 안 나오거든요.
조금 더 들어가 보면
먼저 커넥션 풀이 뭔지부터요.
애플리케이션은 DB에 연결할 때마다 새 연결을 만들지 않습니다. 비싸거든요. 그래서 미리 몇 개(예: 50개)를 만들어두고 돌려씁니다. 요청이 오면 하나 빌려 쓰고 끝나면 반납해요. 이게 커넥션 풀입니다.
"Timeout acquiring a connection"은 빌릴 게 없어서 기다리다 포기했다는 뜻이에요.
그러면 두 가지 중 하나입니다. 너무 오래 빌리고 있거나, 반납을 안 하고 있거나. 이 리포트가 밝힌 건 둘 다였습니다.
🕵️ 가장 좋은 단서 — "재배포하면 1시간 뒤 재발"
리포트에서 추리가 가장 빛나는 대목이 여기입니다.
이 패턴 자체가 증거입니다.
만약 단순히 트래픽이 몰려서 부족한 거라면, 재배포해도 트래픽이 그대로니 금방 다시 터져야 합니다. 그런데 1시간에 걸쳐 서서히 돌아왔어요. 이건 뭔가가 조금씩 새고 있다는 뜻입니다.
"재배포하면 잠깐 낫는다"를 임시방편으로 넘기지 않고 단서로 읽은 것 — 이게 부검의 자세입니다.
1️⃣ 루프 안의 flush
첫 번째 원인이 가장 흔한 안티패턴입니다.
for (const ruleset of rulesets) {
await this.createRulesetRecordWithEm(em, /* ... */); // 내부적으로 flush 발생
}
리포트가 persist와 flush의 차이를 아주 정확히 설명해요.
em.persist(entity)의 동작은 매우 가벼운 인메모리(in-memory) 작업입니다. DB와는 전혀 통신하지 않습니다. (…) 반면, em.flush()는 persist 등으로 등록된 모든 변경사항을 모아서 실제로 DB에 동기화하는 무거운 작업입니다."persist는 메모장에 적는 것, flush는 우체국에 가는 것입니다. 100건을 처리한다면 메모는 100번 해도 되지만 우체국은 한 번만 가면 돼요.
그런데 위 코드는 100번 갑니다. 갈 때마다 커넥션을 빌리고 반납하고요. 리포트가 flush 한 번에 일어나는 일을 여섯 단계로 풀어놓은 것도 좋았습니다. 변경 감지 → SQL 생성 → 트랜잭션 시작 → DB 왕복 → 커밋 → 결과 대기.
고친 형태는 이렇습니다.
// TO-BE: 루프 밖 단일 flush
const entitiesToPersist = [];
for (const item of items) {
const entity = new Entity();
// ...
entitiesToPersist.push(entity);
}
await em.persistAndFlush(entitiesToPersist); // 훨씬 효율적
모아뒀다가 한 번에. 이 패턴은 ORM 종류를 가리지 않고 통합니다.
2️⃣ em.fork() 남용 — 커넥션이 두 배가 된다
두 번째 원인이 이 리포트에서 가장 배울 게 많은 부분이었습니다.
em.fork()는 독립된 작업 공간을 새로 만드는 기능입니다. 문제는 그 공간이 자기 커넥션을 따로 쓴다는 것.
em.fork()를 한 번씩 호출한다고 가정해 보겠습니다. 원본 EntityManager 50개 + fork된 EntityManager 50개 = 최대 100개의 DB 커넥션이 동시에 필요하게 될 수 있습니다."풀 크기가 50이면 나머지 50은 기다리다 타임아웃입니다. 그리고 루프 안에서 부르면 더 나빠집니다.
그런데 리포트는 성능보다 더 위험한 문제를 따로 짚어요.
ORM에는 원칙이 하나 있습니다. 한 작업 공간 안에서 같은 DB 행은 항상 같은 객체라는 것. fork()는 이 원칙을 깹니다.
리포트가 든 시나리오가 명확해요.
원본: userA.name = '김영희' 복제본: userB.name = '김철수' forkedEm.flush() → DB에 '김철수' em.flush() → DB에 '김영희' ← '김철수'가 사라짐
아무 에러도 안 납니다. 그냥 한쪽 수정이 조용히 없어져요. 이걸 업데이트 유실(Lost Update)이라고 합니다.
에러가 나는 문제보다 안 나는 문제가 무섭다는 걸 잘 보여주는 예시입니다.
대안은 @EnsureRequestContext()입니다. 리포트의 설명이 깔끔해요. 컨텍스트가 없으면 만들어주고, 있으면 기존 걸 쓴다. 개발자가 생성·해제 시점을 신경 쓸 필요가 없어집니다.
3️⃣ 롤백 부재 — idle in transaction
세 번째가 직접적인 누수 지점입니다.
try {
await em.begin();
// ... 비즈니스 로직 ...
await em.commit();
} catch (error) {
// 에러 발생 시 rollback 처리가 없음!
}
catch가 비어 있습니다. 에러를 잡기만 하고 트랜잭션을 정리하지 않아요.
그러면 DB 입장에서 그 커넥션은 "BEGIN은 했는데 COMMIT도 ROLLBACK도 안 온 상태"로 남습니다. PostgreSQL에서 idle in transaction이라고 부르는 상태예요. 무기한 대기합니다.
한 번 새면 그 커넥션은 영영 안 돌아옵니다. 그래서 서서히 줄어들다가 한 시간쯤 뒤에 바닥나요. 아까 본 그 패턴과 정확히 맞아떨어집니다.
리포트의 장기 처방이 좋았습니다.
em.begin()의 사용을 가급적 지양하고, 대신 em.transactional(async (em) => { ... }) 콜백 함수 스타일 사용을 권장합니다. 이 방식은 에러 발생 시 자동으로 롤백을 처리해주어 개발자의 실수를 방지합니다."실수할 수 없게 만드는 쪽으로 API를 바꾸는 것입니다. begin/commit을 손으로 쓰면 언젠가 누가 빠뜨려요. 콜백 스타일은 빠뜨릴 자리가 없습니다.
🛡️ "개인의 주의력에 의존하기보다"
장기 방안에 나오는 이 문장이 리포트 전체를 관통합니다.
그리고 구체적인 장치를 붙입니다.
· ESLint 규칙으로 em.fork() 사용 시 경고
· PR 템플릿에 fork() 사용 타당성 설명란 추가
· 루프 안 flush 호출을 잡는 커스텀 린트 규칙 검토
"다음부터 조심합시다"로 끝내지 않았어요. 조심하지 않아도 걸리게 만들었습니다.
관측 쪽도 마찬가지입니다. 리포트에는 커넥션 풀 상태를 1분마다 남기는 코드가 통째로 들어 있어요.
@Cron(CronExpression.EVERY_MINUTE)
logConnectionPoolStatus() {
const knex = this.orm.em.getKnex();
const pool = knex.client.pool;
const poolStats = {
totalCount: pool.getPoolSize(), // 풀의 전체 사이즈
usedCount: pool.numUsed(), // 현재 사용 중인 커넥션 수
freeCount: pool.numFree(), // 유휴 상태(사용 가능)인 커넥션 수
pendingAcquires: pool.numPendingAcquires(), // 커넥션을 기다리는 요청 수
pendingCreates: pool.numPendingCreates(),
};
this.logger.info({ db_pool: poolStats }, 'DB Connection Pool Stats');
}
특히 pendingAcquires가 핵심 지표입니다. 커넥션을 기다리는 요청 수니까요. 이 값이 0보다 커지기 시작하면 타임아웃이 나기 전에 알 수 있습니다.
그리고 왜 이렇게까지 하는지도 설명합니다. Neon DB는 앞단에 커넥션 풀러를 두기 때문에 DB 콘솔에서는 애플리케이션 풀 상태가 안 보인다고요. 보이지 않는 것을 보이게 만드는 작업입니다.
큐레이터 노트
실제 장애가 그대로 담긴 문서라서 뽑았습니다.
장애 회고는 밖으로 잘 안 나옵니다. 부끄럽기도 하고, 내부 정보라서요. 그런데 이런 문서가 가장 배울 게 많습니다. 잘 만든 시스템 이야기보다 무너진 시스템의 단면이 더 많은 걸 알려주거든요.
문서 구조가 뛰어납니다. 원인을 1~4순위로 매겨놨어요. 넷 다 문제지만 무엇을 먼저 고쳐야 하는지가 정해져 있습니다. 그리고 단기와 장기를 나눴어요. 급한 불부터 끄되 체질 개선 계획도 같이 세웠습니다.
동료 이름을 밝힌 것도 좋았습니다. "@최승준_Commerce 님이 PR #531에서", "@이준엽_Commerce 님이 마지막에 수정한 PR #534". 누가 무엇을 고쳤는지 기록하고 있어요. 웹훅 개선은 제환 님 몫이었다고 따로 적기도 했고요.
근거를 다는 습관도 눈에 띕니다. 마지막 장이 통째로 레퍼런스예요. 공식 문서 링크, API 문서 인용, 그리고 GitHub 이슈에서 메인테이너가 뭐라고 답했는지까지. 팀을 설득하려면 이만큼의 근거가 필요하다는 걸 아는 문서입니다.
마지막 결론이 특히 좋았어요.
fork()는 '하지 말아야 할 것'이 아니라 '정확히 알고 써야 하는 것'입니다."도구를 탓하지 않았습니다. 장애 회고가 빠지기 쉬운 함정 하나를 잘 피해갔어요.
03
⏱️ Particify와 LLM으로 2시간 걸릴 퀴즈 준비 10분 만에 끝내기
김종진 님
2025년 하반기에 스터디 세 개를 동시에 운영하게 된 상황에서, 퀴즈 플랫폼을 고르고 문제 준비를 자동화한 기록입니다.
요구사항을 먼저 정하고 후보를 하나씩 떨어뜨려요. Kahoot은 글자 수, Slido는 줄바꿈, AhaSlides는 문제 수와 인원 제한. 남은 게 독일의 Particify였습니다.
그리고 CSV 가져오기/내보내기 + LLM을 붙여 1~2시간 걸리던 준비를 10분으로 줄입니다.
기술 구현 글은 아닙니다. 그런데 도구를 고르고 붙이는 방식에서 배울 게 많은 글이에요.
조금 더 들어가 보면
이 글이 잘한 첫 번째는 순서입니다.
보통은 반대로 합니다. 유명한 도구를 먼저 써보고, 안 맞으면 다른 걸 찾아보고요. 이 글은 요구사항을 먼저 적었습니다.
· 인원: 매주 5명 이상
· 문제: 문제당 700~1,000자의 긴 지문
· 분량: 20개 이상 문제 등록
· 기능: 문제별 타이머
· 비용: 무료
이 다섯 줄이 있으니 판단이 빨라집니다. 써보지 않고도 탈락시킬 수 있어요.
📏 무료 플랜의 제한은 저마다 다른 축에 있다
떨어진 이유들이 흥미롭습니다.
| 도구 | 걸린 제한 |
|---|---|
| Kahoot | 문제 100자, 답안 70자 |
| Slido | 255자, 게다가 줄바꿈 불가 |
| AhaSlides | 퀴즈당 10문제, 참가자 3명 |
제한을 거는 축이 다 다릅니다. 글자 수, 문제 수, 인원 수.
이게 왜 중요하냐면 — 내 요구사항이 어느 축에 걸리는지에 따라 답이 달라지기 때문입니다. 이 스터디는 AWS 자격증 문제를 다뤘어요. 자격증 문제는 지문이 깁니다. 상황 설명이 몇 줄씩 붙거든요. 그러니 글자 수 축이 결정적이었습니다.
같은 도구라도 짧은 퀴즈를 하는 사람에게는 Kahoot이 최선일 수 있어요. 글이 도구를 깎아내리지 않고 "내 조건에 안 맞았다"로 적은 게 정확합니다.
Slido의 줄바꿈 불가를 짚은 것도 눈에 띄었어요. 글자 수만 보면 255자로 부족하지 않아 보이는데, 줄바꿈이 안 되면 코드나 목록이 한 줄로 뭉개집니다. 직접 써보지 않으면 모르는 종류의 제약이에요.
🔑 CSV — 이 글의 진짜 발견
Particify를 고른 뒤에 찾아낸 기능이 판을 바꿉니다.
CSV로 내보내고 들여올 수 있다는 것. 평범해 보이는데 실제로는 두 가지가 딸려옵니다.
첫째, 백업입니다. 글이 짚은 대로 "기간만료로 삭제 또는 실수로 문제세트를 제거하여도 다시 import하여 복원할 수 있다." 무료 서비스는 보관 기간에 제한이 있는 경우가 많은데, 내 손에 파일이 있으면 상관없어요.
둘째, 그리고 이게 더 큽니다 — 입구가 열립니다. 웹 화면에서 하나씩 입력하는 대신, 파일을 만들어서 밀어 넣을 수 있게 됩니다. 그러면 그 파일을 누가 만드느냐가 새로운 질문이 돼요.
🤖 LLM에게 구조를 가르치기
여기서 자동화가 붙습니다.
방법이 구체적입니다.
1. ParticifyCSVImportGuide.md와 예시 CSV 파일을 먼저 전달해 구조를 이해시킨다
2. 과목별 문제 자료를 준비한다
3. 마크다운 파일이나 이미지를 주고 CSV를 생성시킨다
4. import해서 결과를 확인한다
1번이 핵심입니다. "CSV로 만들어줘"라고만 하면 열 이름도 순서도 제멋대로 나와요. 가이드 문서와 실제 예시 파일을 먼저 주면 그 형식을 따라갑니다. 정답 표시 방식, 타이머 값 형식 같은 세부까지요.
그리고 이 팁이 재미있었습니다.
파일을 여러 개 전달하는 동안 매번 요약이나 감상이 돌아오면 거추장스럽습니다. 그래서 "이해했으면 1만 답해"라고 시켜두는 거예요. 준비 단계와 작업 단계를 분리하는 작은 요령입니다.
결과가 명확합니다.
스터디 세 개를 매주 돌리는 상황이었으니, 주당 서너 시간이 30분으로 줄어든 셈입니다. 자동화가 성립하는 조건이 명확해요. 반복되니까요.
🧩 아쉬운 점을 대하는 방식
마지막 장이 이 글에서 가장 좋았습니다. 아쉬운 기능 세 가지를 적어요.
· 문제/답안 순서 섞기 — 반복하면 이해가 아니라 위치를 외우게 됨
· 문제 즐겨찾기 — 틀린 문제만 다시 풀기
· 문제별 질문/답변 — 지금은 주차 단위 Q&A만 가능
그런데 여기서 멈추지 않습니다.
플랫폼에 없는 기능을 CSV 왕복으로 만들어내는 발상입니다.
내보내기 → LLM에게 섞으라고 시키기 → 다시 들여오기. 순서 섞기 기능이 생긴 것과 결과가 같아요. 틀린 문제 모으기도 응답 데이터를 뽑으면 됩니다.
이게 아까 말한 "입구가 열린다"의 진짜 의미입니다. 데이터를 꺼내고 넣을 수 있으면, 그 사이에 무엇이든 끼워넣을 수 있어요.
그리고 이 한 문장이 이 글의 태도를 요약합니다.
큐레이터 노트
고르는 과정 전체를 남긴 글이라 뽑았습니다.
도구 추천 글은 대개 "이거 좋아요"로 시작합니다. 그런데 읽는 사람 입장에서는 "내 상황에도 좋은가"를 알 수 없어요. 이 글은 요구사항 다섯 줄과 탈락 사유를 밝힙니다. 그래서 조건이 다른 사람도 자기 답을 계산할 수 있어요. 짧은 퀴즈면 Kahoot을, 세 명 이하면 AhaSlides를요.
떨어진 도구들을 기록한 게 특히 값집니다. 보통 최종 선택만 적잖아요. 그런데 "Kahoot은 100자 제한" 같은 정보는 직접 가입해서 써봐야 알 수 있는 것들입니다. 남이 쓴 시간을 아껴주는 기록이에요.
퀴즈 링크를 전부 공개한 것도 인상적이었습니다. AWS SAA 2~6주차, 방통대 Unix 시스템. "어떤식으로 작동하는지 궁금할 사람들을 위해"라고요. 말로 설명하는 대신 직접 눌러보게 했습니다.
그리고 이 글은 문제를 자동화한 게 아니라 문제 준비를 자동화했어요. 어느 쪽이 반복인지 정확히 골랐습니다. 스터디의 본질(무엇을 배울지 정하고, 함께 풀고, 이야기하는 것)은 그대로 두고 손이 많이 가는 옮겨 적기만 덜어냈습니다.
💡 세 편을 겹쳐 읽으며
세 글의 영역이 완전히 다릅니다. 브라우저 스펙, 서버 장애, 스터디 운영.
그런데 셋 다 같은 일을 했어요. 경계를 읽고, 선을 그었습니다.
오세명 님은 스펙을 읽고 선을 그었습니다. 핑퐁은 브라우저가 이미 하니 손대지 않는다. 종료 코드도 브라우저에 맡긴다. 대신 bfcache와 재연결은 내 몫이다.
이준호 님은 공식 문서와 메인테이너 답변을 읽고 선을 그었습니다. 컨텍스트 관리는 프레임워크(@EnsureRequestContext)에 맡긴다. 롤백은 콜백 스타일에 맡긴다. 대신 린트 규칙과 모니터링은 우리가 만든다.
김종진 님은 무료 플랜의 제한을 읽고 선을 그었습니다. 문제 형식 변환은 LLM에 맡긴다. 대신 무엇을 물을지, 어떤 도구를 쓸지는 내가 정한다.
세 번 다 방향이 같습니다. 이미 잘 되어 있는 건 맡기고, 아무도 안 해주는 것에 시간을 쓰는 쪽으로요.
그리고 이 선을 그으려면 먼저 읽어야 합니다. 스펙을, 공식 문서를, 요금제 안내를요. 안 읽으면 두 가지 실수가 나옵니다. 이미 있는 걸 또 만들거나(직접 만든 하트비트), 맡겨도 되는 걸 손으로 쥐고 있거나(em.fork()를 직접 관리).
세 글 다 읽는 데 먼저 시간을 썼습니다. 그게 셋 다 깊이 있는 이유예요.
이번 회차에서 가져갈 것
1. 웹소켓 하트비트를 직접 만들기 전에 확인하기.
프로토콜에 Ping/Pong 프레임이 이미 있고 브라우저가 관리합니다. 직접 만든 하트비트는 그냥 텍스트 메시지고, 백그라운드 탭에서 타이머가 느려지면 멈춥니다.
2. 연결이 자꾸 끊기면 프록시 타임아웃을 보라.
유휴 연결을 끊는 기본값이 보통 60초 안팎입니다. 핑 간격이 프록시 타임아웃보다 짧아야 합니다.
3. 1006은 이유를 알려주지 않는다 — 의도된 설계다.
이유를 노출하면 내부 네트워크 구조를 탐지할 수 있어서요. wasClean: false면 일단 재연결을 시도하는 게 실용적입니다. 처음부터 연결이 실패한 경우도 챙기세요.
4. persist는 메모장, flush는 우체국.
루프 안에서 flush를 부르면 반복 횟수만큼 DB를 왕복합니다. 모아뒀다가 루프 밖에서 한 번에 persistAndFlush(배열) 하세요.
5. catch 블록에 롤백이 있는지 확인하기.
없으면 그 커넥션은 idle in transaction으로 영영 안 돌아옵니다. 아예 em.transactional(async (em) => {...}) 콜백 스타일을 쓰면 빠뜨릴 자리가 없어져요.
6. "재배포하면 잠깐 낫는다"는 누수의 신호다.
트래픽 문제라면 재배포해도 금방 재발합니다. 서서히 나빠지는 패턴은 뭔가가 반납되지 않고 있다는 뜻이에요.
7. 주의력 대신 장치를 만들라.
"다음부터 조심합시다"는 다음에 또 일어납니다. 린트 규칙, PR 템플릿, 자동 롤백 API — 실수할 자리를 없애는 쪽으로 가세요.
8. 도구는 요구사항을 적고 나서 고른다.
인원·글자 수·문제 수·기능·비용을 먼저 적으면 써보지 않고도 탈락시킬 수 있습니다. 그리고 떨어진 이유를 기록해두면 조건이 다른 사람에게도 쓸모가 있어요.
9. 데이터를 꺼내고 넣을 수 있으면 기능을 만들 수 있다.
CSV 내보내기/들여오기 하나로 백업, 순서 섞기, 오답 모으기가 다 가능해집니다. 도구를 고를 때 "내 데이터를 가져올 수 있는가"를 꼭 보세요.
10. LLM에게는 예시 파일을 먼저 준다.
"CSV로 만들어줘"보다 가이드 문서 + 실제 예시 파일을 주고 시작하면 형식이 안 흔들립니다. 준비 단계에서는 "이해했으면 1만 답해"로 잡담을 줄이세요.
좋은 글 남겨주신 오세명 님, 이준호 님, 김종진 님께 감사드립니다! 😊
다음 큐레이션으로 다시 찾아오겠습니다! 🌱
- 에디터 · 성장일지 큐레이터 -
※ 본 큐레이션은 각 저자가 공개한 글을 소개하는 것이며, 모든 원문의 저작권은 저자에게 있습니다. 저자 본인의 요청이 있을 경우 즉시 수정 또는 삭제합니다.