JANDI 채팅방 크롤링 — AngularJS 가상 스크롤 SPA를 Puppeteer로 전수 수집한 삽질기

왜 JANDI 채팅방을 크롤링해야 했나 한국거래소(KRX)에서 증권사 담당자들과 소통하는 채널로 JANDI 메신저를 사용하고 있다. “[KRX] 거래시간 연장 및 장애대응 실시간 채팅"이라는 채팅방에서 400여 명의 증권사 담당자들이 질문하고, KRX 측이 답변하는 구조다. 문제는 이 Q&A 내역을 체계적으로 관리할 방법이 없다는 것이었다. JANDI에는 메시지 읽기 API가 없고, Outgoing Webhook은 시작 키워드가 필수라서 모든 메시지를 수신할 수 없다. 결국 브라우저 자동화로 직접 크롤링하는 수밖에 없었다. JANDI의 기술 스택이 만든 함정 JANDI 웹앱은 AngularJS 기반 SPA(Single Page Application)다. 열어보면 URL이 https://next-it.jandi.com/app/#!/room/34791415 같은 해시 라우팅을 쓰고 있다. 이게 크롤링에 어떤 영향을 주는지 처음엔 몰랐다. ...

2026-03-31 · 7분 소요 · Seunghan

Rails 8에서 Devise 걷어내기 — has_secure_password로 마이그레이션한 실전 기록

Devise가 Inertia.js와 싸우기 시작했다 Rails 8 + Inertia.js + Svelte 5 스택으로 운영하던 프로젝트에서 로그인이 안 되는 버그가 터졌다. 에러 메시지도 없고, 401만 돌아왔다. 로그를 보니 Warden의 database_authenticatable strategy가 valid_for_params_auth? = false를 찍고 있었다. 쿼리가 0개 — DB에 접근조차 안 한 것이다. 원인은 Devise의 Warden 미들웨어가 request.params[:user]를 읽는데, Inertia.js는 {email, password}를 flat하게 보내서 Rails의 ParamsWrapper가 session 키로 감싸버리는 구조적 충돌이었다. # Inertia.js가 보내는 것 { email: "user@example.com", password: "secret" } # Rails ParamsWrapper가 변환한 것 { email: "...", password: "...", session: { email: "...", password: "..." } } # Devise/Warden이 찾는 것 { user: { email: "...", password: "..." } } # ← 이게 없다 normalize_sign_in_params라는 핵으로 params[:user]를 세팅했지만, Warden은 ActionController의 params가 아니라 **Rack 레벨의 request.params**를 따로 읽었다. 두 객체는 완전히 별개다. ...

2026-03-31 · 7분 소요 · Seunghan

Mole로 맥 190GB 정리한 후기 — 개발자 맥에 쌓인 캐시의 실체

개발하다 보면 디스크가 어느 순간 꽉 찬다. Xcode 빌드하다 용량 부족 에러가 뜨고, Docker 이미지 pull이 실패하고, npm install이 ENOSPC를 뱉는다. 저장소 확인해보면 “시스템 데이터"가 수백 GB를 차지하고 있는데, 정작 뭐가 그렇게 큰지 알 수가 없다. CleanMyMac 같은 도구가 있지만 연 9만원짜리 구독이고, 개발자 특화 캐시는 잘 못 잡는다. 그러다 GitHub에서 34K 스타를 받은 오픈소스 CLI 도구 Mole을 발견했다. 설치부터 190GB 정리까지의 실전 기록을 남긴다. Mole이 뭔가 Mole은 대만 개발자 tw93이 만든 macOS 시스템 정리 CLI 도구다. Shell 79%와 Go 21%로 작성됐고, MIT 라이선스다. ...

2026-03-30 · 7분 소요 · Seunghan

Render 배포 실패 두 가지 — npm lockfile dev 플래그와 DATABASE_URL 소켓 오류

Render에서 배포가 갑자기 안 되기 시작했다. 한 번도 아니고 두 번 연속으로, 서로 다른 이유로. 첫 번째는 Vite 빌드에서 패키지를 못 찾는다는 에러, 두 번째는 Rails 서버가 PostgreSQL에 소켓으로 연결하려다 죽는 오류였다. 둘 다 원인을 찾기까지 상당히 헤맸다. 첫 번째 오류: Cannot find package 'vite-plugin-ruby' 빌드 로그에 이런 에러가 찍혔다. failed to load config from /opt/render/project/src/.../vite.config.ts error during build: Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'vite-plugin-ruby' imported from vite.config.ts vite-plugin-ruby는 분명히 package.json의 dependencies에 들어있었다. devDependencies가 아니라 dependencies에. ...

2026-03-30 · 4분 소요 · Seunghan

Flutter AAB 빌드 서명 오류 해결 — Play Store 업로드 시 wrong key 에러 완벽 가이드

Flutter AAB를 Play Console에 드래그했더니 서명 에러 Flutter로 만든 앱을 Google Play Console에 업로드하려고 flutter build appbundle --release로 AAB 파일을 빌드했다. 빌드 자체는 성공했고, 51MB짜리 app-release.aab가 잘 생성됐다. 자신만만하게 Play Console에 드래그 앤 드롭했는데, 이런 에러가 떴다. Android App Bundle이 잘못된 키로 서명되었습니다. 제대로 된 서명 키로 App Bundle에 서명한 다음 다시 시도해 보세요. SHA1: 5A:2A:F8:A4:71:76:3B:CC:35:78:33:B1:98:65:8F:24:85:72:AB:87 지문이 포함된 인증서로 App Bundle에 서명해야 하지만, 업로드한 App Bundle 서명에 사용된 인증서의 지문은 SHA1: A8:E9:B6:3C:C6:9A:E9:FE:06:AA:BB:2E:E3:43:85:1A:74:96:16:48 입니다. 두 개의 SHA1 지문이 달랐다. Play Console이 기대하는 키와, 실제 AAB에 서명된 키가 다르다는 뜻이다. ...

2026-03-26 · 7분 소요 · Seunghan

Flutter에서 Gemini, OpenAI, Claude 직접 연동하기 — 멀티 AI 프로바이더 패턴 구현

시작 — 하드코딩된 API 키 문제 Flutter 앱에서 AI 기능(영수증 OCR, 이미지 번역, 블로그 자동 생성)을 넣을 때, 처음에는 BizRouter라는 AI 프록시 서비스를 썼다. 모든 요청을 하나의 엔드포인트로 보내면 내부에서 Gemini, GPT, Claude 등으로 라우팅해주는 구조였다. 문제는 API 키가 소스 코드에 하드코딩되어 있다는 것이었다. class BizRouterService { static const _apiKey = 'sk-br-v1-d6872ae8e164...'; // 이게 코드에 그대로 static const _baseUrl = 'https://api.bizrouter.ai/v1'; 지인들에게 배포하는 MVP라 처음에는 괜찮았지만, 사용자가 자기 키를 입력해서 쓸 수 있게 만들어야 했다. Gemini 키가 있는 사람은 Gemini로, OpenAI 키가 있는 사람은 GPT로, Claude 키가 있는 사람은 Claude로 — 각자 가진 키를 쓸 수 있어야 했다. ...

2026-03-26 · 9분 소요 · Seunghan

Flutter에서 iOS Live Activity 잠금화면 미리보기 만들기 — CustomPainter로 네이티브 위젯 재현

왜 Live Activity 미리보기가 필요했나 여행 앱에서 iOS Live Activity를 구현하고 나면, 사용자에게 표시 모드를 선택하게 하는 설정 화면이 필요하다. 일정 모드, 예산 모드, 자동 모드, 결합 모드 — 이렇게 네 가지 옵션이 있는데, 문제는 이 모드를 바꿨을 때 잠금화면에서 실제로 어떻게 보이는지 사용자가 알 수 없다는 점이었다. 설정 화면 하단에 Dynamic Island compact 형태의 간단한 미리보기는 있었다. 하지만 사용자가 Live Activity를 가장 많이 보는 곳은 잠금화면이다. 작은 알약 모양 미리보기로는 “이 모드를 선택하면 잠금화면이 이렇게 바뀝니다"를 전달하기 어려웠다. ...

2026-03-26 · 8분 소요 · Seunghan

Godot 4 미니게임 모드 전환 — 상태 저장/복원 패턴과 _process() 함정

다마고치 게임에 미니게임 3종을 넣었다. 게임 자체보다 어려웠던 건 게임 모드 전환이다. 미니게임을 시작하면 씬이 줌아웃되고 캐릭터가 축소되고 이동 범위가 바뀌고 배경이 교체된다. 게임이 끝나면 이 모든 게 원래대로 돌아와야 한다. 하나라도 복원을 빠뜨리면 버그가 된다. 캐릭터가 작은 채로 남아있거나, 배경이 확장된 채로 유지되거나, 이동 범위가 좁아진 채로 고정된다. 유저는 “게임이 망가졌다"고 느낀다. 이 글에서는 미니게임 모드 전환 시 상태 관리 패턴과, 실제로 터진 버그들의 원인과 해결을 정리한다. 문제의 본질: 임시 상태 vs 영구 상태 미니게임 모드는 임시 상태다. 잠깐 동안만 다른 설정을 쓰고, 끝나면 원래대로 돌아간다. 이게 간단해 보이는데 실제로는 복잡하다. ...

2026-03-26 · 7분 소요 · Seunghan

앱스토어 마케팅 스크린샷 자동화 — Puppeteer + features.json 분석 주도 파이프라인

앱 출시를 앞두고 스토어 스크린샷을 만들어야 하는 상황이 됐다. Figma로 하나씩 만드는 건 너무 비효율적이고, 10가지 디자인 시안에 5개 기능 화면을 조합하면 50장인데 수작업으로 한다는 건 말이 안 된다. 그래서 HTML/CSS로 마케팅 프레임을 만들고 Puppeteer로 PNG를 뽑아내는 파이프라인을 짰다. 처음엔 대충 만들었다가 구조적으로 틀린 부분이 있다는 걸 나중에 깨달았는데, 그 과정이 의외로 중요한 교훈을 남겼다. 처음 접근 방식의 문제 처음엔 이런 식으로 기능 배열을 하드코딩했다. const FEATURES = [ { id: 'expense', title: '지출을 한번에', sub: 'AI가 영수증을 읽어드립니다', screen: '02_expense_detail' }, { id: 'camera', title: '사진 찍으면 끝', sub: 'OCR로 즉시 기록', screen: '03_camera_hub' }, // ... ]; 문제는 이 파일명들(02_expense_detail.png, 03_camera_hub.png)이 실제로 존재하지 않았다는 것이다. 캡처 스크립트가 자동으로 화면을 이동하다가 실패해서 홈 화면만 5번 찍혔는데, 파일명은 다 달랐다. 결과적으로 모든 슬롯이 placeholder(검은 화면)였다. ...

2026-03-26 · 6분 소요 · Seunghan

git log -S로 UI가 언제 바뀌었는지 추적하기 — pickaxe, blame, show 실전 조합

사이드바 타이틀이 이상하다 Rails + Hotwire Native 기반 웹앱을 운영하고 있다. 어느 날 사이드바 상단에 원래 표시되던 앱 타이틀 대신 “리서치·정보분석” 같은 팀 이름이 들어가 있는 걸 발견했다. 분명 예전에는 앱 이름이 나왔는데 언제 바뀐 건지 모르겠다. 이런 상황, 개발하다 보면 꽤 자주 겪는다. “이거 원래 이랬나?” 싶은 순간이 오면, 코드를 뒤지기 전에 Git 히스토리를 먼저 파보는 게 훨씬 빠르다. 이번에 실제로 git log -S, git show, git diff를 조합해서 원인 커밋을 찾아낸 과정을 정리했다. ...

2026-03-25 · 6분 소요 · Seunghan
개인정보처리방침 이용약관 면책조항 문의