본문 바로가기

반응형

전체 글

(80)
6편: SSH 배포가 계속 죽어서 러너를 직접 만들었다 Docker 빌드 중간에 죽는 배포기능 개발과 별개로, 배포 자동화도 필요했습니다. 처음엔 GitHub Actions에서 SSH로 서버에 접속해서 docker compose build를 실행하는 흔한 방식으로 시작했습니다.문제는 admin 이미지 빌드가 오래 걸리면 SSH 세션이 아무 데이터도 안 오가는 상태(idle)로 몇 분씩 유지된다는 점이었습니다. 그러다 어느 순간 i/o timeout으로 세션이 끊기고 배포가 실패했습니다. 타임아웃 값을 늘려도 소용없었습니다. TCP 레벨에서 idle 세션을 끊는 문제라, 애플리케이션 타임아웃 설정으로는 손댈 수 없는 영역이었습니다.SSH 기반 배포는 오래 걸리는 Docker 빌드에 근본적으로 취약하다. Idle TCP 세션이 네트워크 레벨에서 끊기는 건 타임아..
3편: 베이지안 스무고개는 어떻게 다음 질문을 고를까 스무고개의 핵심은 "확률을 갱신하는 것"이 엔진은 베이지안 추론으로 돌아갑니다. 어렵게 들리지만 아이디어는 단순합니다. 처음엔 모든 메뉴가 후보이고, 사용자가 질문에 답할 때마다 각 메뉴가 정답일 확률을 조금씩 조정합니다. "매운가요? → 예"라고 답하면 매운 메뉴들의 확률은 올라가고, 안 매운 메뉴들은 내려가는 식이죠.각 메뉴가 어떤 질문에 "예"라고 답할 확률은 라플라스 스무딩을 얹어 계산합니다. jsconst pYesOf = (m, q) => (m.yes[q] + 1) / (m.yes[q] + m.no[q] + 2);+1, +2가 라플라스 스무딩입니다. 데이터가 적은 초반에 확률이 0이나 1로 튀어버리는 걸 막아주는 안전장치예요.답은 예/아니오만 있는 게 아닙니다사람 마음이 늘 딱 떨어지진 않죠. ..
5편: "함께 보면 좋아요"를 다형적 구조로 갈아엎다 증상 가이드는 자유 본문이 아니라 정형 폼가이드가 Quill 자유 본문이었다면, 증상 가이드는 정반대로 정형 폼입니다. 대표질환/원인, 집에서 체크할 것, 팁, 참고자료(정상/비정상 사진 묶음) 같은 구조화된 항목을 리스트로 관리해야 했습니다. 그래서 자식 테이블을 5개 두는 구조(symptom_suspected_diseases, symptom_home_checks, symptom_tips, symptom_references, symptom_reference_images)로 V4에서 새로 만들었습니다.연관 관계, 처음엔 단방향이었다처음 만든 "함께 보면 좋아요" 연관은 증상 가이드가 다른 증상 가이드만 가리키는 단방향 구조였습니다. 그런데 실제로 필요한 건 증상 가이드에서 일반 가이드로도, 가이드에서 증..
2편: 엔진은 브라우저에 두고, 서버는 딱 세 가지만 가장 먼저 정한 것: 엔진을 어디에 둘까메뉴를 맞히는 "머리" 역할을 하는 엔진(질문을 고르고, 답을 반영해 후보를 좁히는 로직)을 어디에 둘지가 첫 번째 결정이었습니다. 서버에 두는 게 일반적인 그림이지만, 저는 브라우저(클라이언트) 에 두기로 했습니다.이유는 코드 주석에 그대로 남겨뒀습니다."엔진은 클라이언트에 그대로 둔다. 질문마다 서버를 왕복하면 체감이 나빠지고, 이미 223개 전수 검증을 통과한 코드를 건드릴 이유도 없다. 서버가 맡는 일은 셋이다: 데이터 공급, 학습 영속화, 관측 로그."정리하면 두 가지입니다. 첫째, 질문 하나 답할 때마다 서버를 왕복하면 반응이 느려집니다. 스무고개는 리듬이 생명인데, 매 질문마다 로딩이 걸리면 재미가 확 깎이죠. 엔진이 브라우저 안에 있으면 답을 누르는 ..
4편: Quill 에디터를 내 마음대로 뜯어고치기 왜 Quill인가가이드 콘텐츠는 리치 텍스트가 필요했습니다. 이미지, 글자색, 체크리스트까지 들어가야 했는데, Next.js 16 + React 19 조합에서 SSR과 충돌하는 에디터가 많았습니다. 결국 quill@2.0.3 코어를 직접 붙이고, dynamic(() => import(...), { ssr: false })로 클라이언트 전용 컴포넌트로 감쌌습니다.새니타이징이 에디터 기능을 결정한다가이드 본문은 백엔드에서 jsoup Safelist로 한 번 걸러집니다. 그런데 이 필터가 뭘 허용하느냐가 거꾸로 에디터에서 뭘 만들 수 있는지를 결정했습니다.private static final Safelist QUILL_SAFELIST = Safelist.none() .addTags("p", "br..
1편: "아무거나"라고 말하는 그 마음 시작은 아주 흔한 장면이었습니다"오늘 뭐 먹을까?" "음… 아무거나."이 대화, 다들 한 번쯤 해보셨을 겁니다. 와이프든, 애인이든, 오랜만에 만난 여사친이든 — "아무거나"라는 대답이 돌아오는 순간이 있죠. 그런데 우리 모두 알고 있습니다. 그 "아무거나"는 정말 아무거나가 아니라는 걸요. 분명히 먹고 싶은 게 있는데, 그게 뭔지 본인도 딱 집어내지 못하는 상태에 가깝습니다.저는 이 지점이 재밌었습니다. 답은 이미 마음 어딘가에 있는데, 질문을 잘 던져주면 꺼낼 수 있는 것. 이거 어디서 많이 본 구조인데요.아키네이터가 떠올랐습니다혹시 아키네이터(Akinator) 아시나요? 머릿속으로 인물 하나를 떠올리면, 스무고개식 질문을 던져가며 그게 누군지 맞혀내는 웹 서비스입니다. "실존 인물인가요?" "남자인..
3편: 회원 관리 화면에서 만난 첫 삽질들 GradeSelect, 계속 초기값으로 돌아오다회원 목록에서 등급을 바꾸는 드롭다운(GradeSelect)을 만들었는데, 값을 바꿔도 화면이 원래 값으로 돌아오는 문제가 계속 났습니다. 커밋 로그를 보면 하루 동안 이 문제만 붙잡고 있었다는 게 그대로 드러납니다. fix: 등급 변경 후 화면 갱신 수정fix: 등급/역할 변경 후 화면 갱신 수정fix: 등급 변경 후 화면 갱신 수정fix: GradeSelect 초기값 수정fix: GradeSelect 변경 후 값 유지 수정fix: GradeSelect 컴포넌트 재작성fix: GradeSelect 초기값 백엔드 응답값으로 설정같은 문제를 다른 각도로 계속 건드린 흔적입니다. 결국 원인은 서버 컴포넌트가 내려준 초기값과 클라이언트 상태가 갱신 타이밍에 어긋나..
2편: 기술 스택 고르기 — Spring Boot, Next.js, 그리고 flyway 백엔드: Spring Boot + PostgreSQL + flyway백엔드와 DB, 인프라는 제가 혼자 맡은 영역이라 "익숙한 것 vs 검증된 것" 사이에서 검증된 것을 택했습니다. Spring Boot에 Spring Data JPA, PostgreSQL, 그리고 스키마 관리는 flyway로 시작했습니다.spring.jpa.hibernate.ddl-auto=validate로 못박아 뒀습니다. Hibernate가 알아서 테이블을 바꾸게 두지 않고, 스키마 변경은 반드시 V{n}__{description}.sql 마이그레이션 파일로만 하기로 한 겁니다. 배포된 마이그레이션은 절대 수정하지 않고 새 버전을 추가하는 것도 초반부터 정한 규칙입니다.실제로 이 프로젝트는 지금 V11까지 왔습니다.V1: users,..

반응형