fetch() + PATCH + 302 Redirect = 보이지 않는 버그

Stimulus 컨트롤러에서 badge 선택 UI를 만들었다. 옵션을 클릭하면 fetch()로 PATCH를 보내고, 서버가 업데이트한 뒤 성공/실패를 표시하는 단순한 구조다. 그런데 DB는 업데이트되는데 UI가 실패 표시를 하면서 원래 값으로 되돌아갔다. 서버 로그를 열기 전까지는 원인을 전혀 짐작할 수 없었다. 증상 badge를 클릭하면: 잠깐 선택 스타일이 바뀜 곧바로 원래 값으로 revert 에러 인디케이터(X) 표시 다른 필드(모드, 대진표 유형)는 정상 동작하는데, 특정 필드만 실패했다. 모델 validation 문제도 아니고, 권한 문제도 아니었다. 서버 로그에서 본 진짜 원인 Started PATCH "/resources/54" for ::1 Processing by ResourcesController#update as TURBO_STREAM Parameters: {"resource"=>{"field_name"=>"new_value"}, "id"=>"54"} ... UPDATE "resources" SET "field_name" = 1 WHERE "id" = 54 COMMIT Redirected to http://localhost:3000/resources/54/dashboard Completed 302 Found in 22ms Started PATCH "/resources/54/dashboard" for ::1 ActionController::RoutingError (No route matches [PATCH] "/resources/54/dashboard"): DB 업데이트는 성공했다. 그런데 서버가 redirect_to dashboard로 302를 보냈고, fetch가 그 redirect를 따라가면서 PATCH method를 유지한 채 dashboard URL로 요청을 보냈다. Dashboard는 GET만 받으므로 RoutingError가 터졌다. ...

2026-03-20 00:00 · 5분 소요 · Seunghan

Tailwind v4 테마 적용 삽질기 — CSS 변수가 안 먹는 진짜 이유

Rails + Tailwind CSS v4 프로젝트에서 그랜드슬램 테마 시스템을 만들었다. 설정에서 Wimbledon(보라), Roland Garros(오렌지), US Open(네이비), Australian Open(스카이블루)을 고르면 앱 전체 색상이 바뀌는 기능이다. 이틀을 날렸다. 결론부터 말하면 CSS 파일 로드 순서 한 줄이 문제였다. 배경: Tailwind v4의 CSS 변수 컴파일 방식 Tailwind v4는 v3과 완전히 다른 방식으로 색상을 처리한다. 가장 중요한 변화는 모든 유틸리티 클래스가 CSS 변수를 통해 동작한다는 것이다. /* Tailwind v4가 bg-emerald-600을 컴파일한 결과 */ .bg-emerald-600 { background-color: var(--color-emerald-600); } v3에서는 bg-emerald-600이 background-color: #059669 같은 하드코딩 hex로 컴파일됐다. v4에서는 CSS 변수 참조로 바뀌었다. 이 차이가 테마 시스템의 핵심이다. ...

2026-03-19 00:00 · 7분 소요 · Seunghan

Telegram 봇 Inline Keyboard 버튼이 무반응인 버그 — UUID Regex + Solid Cache 디버깅

Telegram 봇에서 자연어 입력 → AI 분석 → Inline Keyboard 확인 버튼 방식으로 할 일을 추가하는 기능을 운영하던 중, 버튼을 눌러도 아무 반응이 없는 증상이 발생했다. 증상 사용자가 자연어로 일정을 입력하면 봇이 다음처럼 확인 메시지를 보낸다. 📝 할 일을 추가할까요? (개인일정) "부장님 점심식사" [📅03/24 ⏰12:00] [✅ 추가] [❌ 취소] 그런데 [✅ 추가] 버튼을 눌러도 응답이 없었다. Telegram 클라이언트에는 “알 수 없는 요청입니다.” 라는 토스트 메시지만 표시됐다. 서버 로그 확인 서버 쪽 webhook 로그를 보면 버튼 클릭은 정상적으로 서버에 도달하고 있었다. ...

2026-03-11 00:00 · 3분 소요 · Seunghan

배포는 됐는데 앱이 죽는다 — Solid Queue가 Puma를 끌고 내려간 이야기

Render에 Rails 앱을 새로 배포했다. 빌드는 성공했고 “Deploy live” 메시지도 떴다. 그런데 몇 분 뒤 대시보드에 이런 메시지가 반복됐다. Instance failed: wcvg7 Application exited early while running your code. 증상 파악 Render 로그를 뒤지니 이런 흐름이 보였다. SolidQueue::Configuration#ensure_configured_processes ← 여기서 에러 → exit 1 → "Detected Solid Queue has gone away, stopping Puma..." → Puma 종료 → 인스턴스 실패 Puma가 죽은 게 아니었다. Solid Queue가 먼저 죽고, Puma가 그걸 감지해서 스스로 내려간 것이었다. ...

2026-03-10 00:00 · 3분 소요 · Seunghan

Chrome 확장 개발 삽질 모음 — 도메인 화이트리스트, 이벤트 리스너 중복, 클로저 함정

Chrome 확장을 유지보수하다 보면 “분명히 동작해야 하는데 왜 안 되지?“라는 상황을 꽤 자주 만난다. 이번에 짧은 시간 안에 4가지 실수를 연달아 저질렀고, 각각 원인이 달랐다. 기록해둔다. 1. 디스패치 블록의 return이 범용 감지를 막는다 Content script 끝부분에는 보통 이런 패턴이 있다. if (isSomeSpecificPage()) { doSomethingSpecific(); return; // ← 여기서 끝 } // 범용 DOM 감지 (MutationObserver 등) const observer = new MutationObserver(() => { ... }); observer.observe(document.body, { childList: true, subtree: true }); 특정 도메인에서만 동작하는 기능을 추가하면서 return으로 빠져나왔더니, 그 도메인의 팝업 창에서 범용 DOM 감지가 아예 실행되지 않았다. ...

2026-03-09 00:00 · 4분 소요 · Seunghan

Flutter BottomSheet가 네비게이션 바를 덮는다면 — showDialog로 바꿔야 하는 이유

폼 입력이 필요한 화면에서 showModalBottomSheet를 쓰다 보면 자연스러운 UX처럼 느껴진다. 그런데 앱에 하단 네비게이션 바가 있으면 바텀시트가 올라오면서 네비게이션을 덮어버리는 문제가 생긴다. 기능적으로는 동작하지만, 시각적으로 답답하다. 세 가지 문제를 한 번에 해결했다. 바텀시트 → 중앙 모달 전환 TextButton 취소 버튼이 노란색으로 렌더링되어 안 보이는 가독성 문제 share_plus로 SQLite 파일 공유 시 발생하는 PlatformException 문제 1: BottomSheet가 네비게이션 바를 가린다 현상 showModalBottomSheet로 만든 입력 폼이 올라올 때 하단 네비게이션 바와 겹친다. isScrollControlled: true를 써도 시트가 네비게이션 위까지 올라와 버린다. ...

2026-03-09 00:00 · 4분 소요 · Seunghan
Render Deploy Debugging

Render 배포 실패 디버깅 — DB 업그레이드부터 Gemfile 누락까지 10개 에러 연속 격파

오늘 Rails 앱 배포가 build_failed로 떨어졌다. 처음엔 단순한 에러 하나겠지 싶었는데, 고칠 때마다 새 에러가 튀어나왔다. 결국 10개의 에러를 순서대로 해결하고 나서야 live 상태가 됐다. 연속 디버깅의 기록을 남긴다. 배경 Render에서 Rails 8 + Inertia.js + Svelte 5 조합 웹 서비스를 운영 중이다. 어느 날 대시보드를 보니 최신 배포가 build_failed 상태. 로그를 열었다. 에러 1: DB 연결 실패 — ActiveRecord::ConnectionNotEstablished bin/rails aborted! ActiveRecord::ConnectionNotEstablished: connection to server at "10.x.x.x", port 5432 failed: Connection refused Tasks: TOP => db:migrate 빌드 스크립트에서 db:migrate를 실행하는 순간 PostgreSQL 연결이 거부됐다. 트리거를 보니 service_resumed — 서비스가 재개(resume)된 것이었다. ...

2026-03-07 00:00 · 5분 소요 · Seunghan
Render Multi Service Error Fix Deploy

Render 6개 서비스 에러 일괄 점검 & 수정 — Stoplight, FK 제약, Puma 7, Solid Stack 삽질 기록

Render에 올려둔 Rails 서비스 6개가 전부 각자 다른 에러를 토해내고 있었다. 하나씩 로그를 까보니 공통 패턴도 있고, 프로젝트마다 고유한 문제도 있었다. 한 세션에서 전부 수정하고 배포까지 마친 과정을 정리한다. 전체 상황 Render API로 서비스 6개의 로그를 일괄 조회했다. 각 서비스에 SSH로 하나씩 들어가서 로그를 보는 대신, Render의 REST API를 활용하면 로컬 터미널에서 모든 서비스의 로그를 한꺼번에 스크립트로 수집할 수 있다. 이번 점검에서 확인된 결과는 다음과 같았다: 서비스 주요 에러 서비스 A ERB 문법 에러로 500 (이미 커밋됐지만 미배포) 서비스 B Stoplight Light#run 블록 에러 + Telegram 파싱 에러 서비스 C solid_cache_entries 테이블 누락 서비스 D PG::UndefinedColumn + solid_cache 누락 서비스 E PG::DuplicateTable sessions + Sentry 초기화 에러 서비스 F TaskCleanupJob FK 위반 + Puma deprecated 경고 공통 패턴: Rails 8의 Solid Stack (SolidCache, SolidQueue, SolidCable) 초기 설정 문제가 여러 프로젝트에서 반복됐다. Render의 무료/스타터 플랜에서 단일 PostgreSQL 인스턴스를 여러 Rails 컴포넌트가 함께 사용하는 구성이 원인이었다. ...

2026-02-24 00:00 · 10분 소요 · Seunghan
Rails Sso Turbo Drive Debugging

Rails SSO 구현 중 Turbo Drive가 유발한 두 가지 버그 디버깅

Rails 앱 간 SSO(Single Sign-On)를 HMAC 기반으로 구현하던 중 예상치 못한 두 가지 버그를 만났다. 둘 다 Turbo Drive와 ERB의 동작 방식에서 비롯된 문제였다. 에러 메시지는 동일하게 “state mismatch"였지만 원인은 전혀 달랐고, 첫 번째 버그를 고쳐도 두 번째가 남아 있어 디버깅이 꽤 번거로웠다. 구현 개요 구조 두 개의 독립적인 Rails 앱이 SSO로 연결된다. IdP (Identity Provider): 사용자 인증을 담당하는 Rails 앱. OTP 로그인을 처리하고 One-Time Token을 발급한다. SP (Service Provider): IdP에서 발급받은 토큰으로 로그인하는 Rails 앱. 직접 사용자 자격증명을 다루지 않고 IdP를 신뢰한다. 이 구조는 소규모 멀티 앱 환경에서 공통 인증을 구현할 때 자주 쓰이는 패턴이다. OAuth 2.0보다 단순하지만 CSRF 방지와 토큰 검증은 동일하게 필요하다. ...

2026-02-13 00:00 · 7분 소요 · Seunghan
Rails Rfc3161 Tsa Blockchain Merkle Debugging

RFC 3161 TSA 타임스탬프 + 블록체인 Merkle 앵커링: Rails에서 삽질 기록

전자계약 보관 시스템에 법적 증거력을 부여하기 위해 두 가지를 동시에 구현해야 했다: 블록체인 Merkle Tree 앵커링 — 계약 해시들을 모아 Merkle Root를 L2 체인에 기록 RFC 3161 TSA 타임스탬프 — 신뢰할 수 있는 제3자 시간 증명 간단해 보였는데, 삽질의 연속이었다. 각 문제를 해결하는 데 예상보다 훨씬 많은 시간이 걸렸고, 특히 Ruby 4.0의 API 변경과 Rails 8의 멀티 데이터베이스 동작이 예상치 못한 방식으로 얽혔다. 1. RFC 3161 TSA란? RFC 3161은 Time-Stamp Authority(TSA) 프로토콜로, 특정 데이터가 특정 시점에 존재했음을 제3자가 증명해주는 국제 표준(RFC 3161, RFC 5816로 업데이트)이다. 법적 맥락에서는 “이 문서가 이 날짜에 존재했음"을 공인 제3자가 서명으로 보증한다는 의미다. ...

2026-02-06 00:00 · 8분 소요 · Seunghan
Rails Project Health Check 553 Tests

Rails 프로젝트 정밀 점검 — 16개 테스트에서 553개, 숨어있던 버그 8개

운영 중인 Rails 8 API 서버를 점검하기로 했다. 기능은 대부분 동작하고 있었지만, 테스트 커버리지가 3%밖에 안 되는 상태. “동작하니까 괜찮겠지"라는 생각이 얼마나 위험한지 확인하는 과정이었다. 프로젝트가 어느 정도 성숙기에 접어들면 기능 개발보다 안정화가 더 중요해진다. 테스트가 없는 코드베이스에서는 리팩토링도, 의존성 업그레이드도, 팀원 온보딩도 전부 도박이 된다. 이번 점검은 단순히 테스트 커버리지 수치를 올리는 작업이 아니라, 현재 코드베이스의 실제 상태를 정직하게 들여다보는 과정이었다. 점검 전 상태 Rails 8 + PostgreSQL (UUID PK) + JWT 인증 + Pundit 권한 RSpec 테스트: 16개 (기본 scaffold 수준) 모델 20개+, 컨트롤러 15개+, 서비스 5개+ Dockerfile은 배포용으로 작성되어 있었고, CI는 없음 테스트 16개가 있다고는 하지만, 실제로는 scaffold 생성 시 자동으로 만들어진 기본 라우팅 테스트 수준이었다. 핵심 비즈니스 로직, 권한 체크, 서비스 레이어는 전혀 커버되지 않은 상태. 이 상태로 몇 달을 운영하면서 기능을 계속 추가해왔다는 게 솔직히 불안했다. ...

2026-02-03 00:00 · 7분 소요 · Seunghan
Chrome Extension Oacx Iframe Korean Ime Fix

Chrome 확장 프로그램에서 iframe OACX 자동입력이 안 되는 문제 — 타이밍과 한글 IME

Chrome 확장 프로그램으로 정부 사이트 간편인증(OACX) 폼을 자동입력하는 기능을 만들었다. 대부분의 사이트에서 잘 동작하는데, 특정 대형 사이트에서 “이름 입력이 안 됩니다"라는 피드백이 들어왔다. 증상 간편인증 팝업이 열리면 이름, 생년월일, 휴대폰번호를 자동입력하는 확장 대부분의 정부 사이트(정부24, 건강보험 등)에서는 정상 동작 특정 사이트에서만 이름 필드가 비어있음 — 생년월일, 전화번호도 안 채워짐 조사: Playwright로 실제 DOM 구조 확인 사용자가 알려준 페이지를 Playwright MCP로 직접 열어서 확인했다. 1단계: 메인 페이지 스냅샷 메인 페이지에서 “간편인증” 버튼을 클릭하면 레이어 팝업 + iframe이 열린다. ...

2026-01-23 00:00 · 5분 소요 · Seunghan
Calendar Print Browser Print Bug Paper Sizes

웹 캘린더 인쇄 기능의 함정: window.print()는 off-screen 엘리먼트를 무시한다

웹에서 캘린더를 출력하는 기능을 만들었다. PDF와 PNG 다운로드는 완벽한데, 브라우저 인쇄 버튼만 누르면 이미지 위치가 전혀 반영되지 않았다. 같은 데이터를 쓰는데 왜 결과가 다를까? 구조: 프리뷰와 숨겨진 내보내기 타겟 캘린더 출력 페이지의 구조는 이렇다: ┌─ 화면에 보이는 영역 ─────────────────┐ │ [설정 패널] [프리뷰 영역] │ │ - 기간 선택 캘린더 미리보기 │ │ - 테마/색상 │ │ - 이미지 위치 슬라이더 │ └──────────────────────────────────────┘ ┌─ 숨겨진 내보내기 타겟 ───────────────┐ │ <div class="fixed -left-[9999px]"> │ ← 화면 밖 │ <PrintableCalendar ... /> │ │ </div> │ └──────────────────────────────────────┘ 프리뷰는 축소된 미리보기고, 실제 내보내기용 캘린더는 원본 크기로 화면 밖(-left-[9999px])에 렌더링된다. PDF/PNG는 이 숨겨진 엘리먼트를 캡처한다. ...

2026-01-20 00:00 · 4분 소요 · Seunghan
Rails Turbo Actioncable 500 Debug

Rails Turbo Stream 500 에러 3종 세트 디버깅 — broadcast, SolidCable, Telegram Markdown

Rails 8 + Hotwire(Turbo) 기반 앱을 운영하다 보면 broadcast_append_to 계열 콜백이 조용히 500을 내뱉는 경우가 있다. 거기에 SolidCable 초기 설정 문제와 Telegram Bot 메시지 파싱 오류가 겹치면 로그 해석도 헷갈린다. 이번에 세 가지가 한꺼번에 터져서 순서대로 해결한 과정을 정리한다. 이 글에서 다루는 세 문제는 서로 독립적이지만, 실제 운영 환경에서는 이렇게 한꺼번에 맞닥뜨리는 경우가 많다. 각 문제를 격리해서 하나씩 해결하는 접근이 중요하다. 문제 1: No unique index found for id — broadcast 콜백 500 현상 메시지나 알림을 생성할 때 컨트롤러에서 500이 발생한다. 로그를 보면: ...

2026-01-09 00:00 · 7분 소요 · Seunghan
Rails Ruby3 Kwargs Dispatch Integration Debug

하루 종일 삽질한 것들 — Ruby 3.0 kwargs, Docker env, NAS 크론, SSH 특수문자

AI 에이전트가 Rails API 서버를 호출해서 티켓을 자동 배정하는 디스패처를 만들었다. 로직 자체는 간단한데 붙이는 과정에서 예상치 못한 곳에서 계속 막혔다. 하루 동안 7개의 서로 다른 버그를 순서대로 만났고, 각각은 사소하지만 연속으로 터지니 꽤 피로했다. 비슷한 스택을 쓰는 사람에게 도움이 됐으면 해서 기록해 둔다. 1. Ruby 3.0 kwargs 분리 — render_success(key: val) 가 왜 터지나 가장 오래 고생한 것. Rails 컨트롤러에서 응답 헬퍼를 이렇게 호출했다: render_success(tickets: tickets_list, pagination: pagination_data) 서버 로그에 찍힌 에러: ...

2026-01-02 00:00 · 7분 소요 · Seunghan
Rails Devise Multistep Signup Resend Email

Rails 8 + Devise 다단계 회원가입 & Resend 이메일 삽질 기록

Rails 8 + Inertia.js + Svelte 5 스택에서 역할별 다단계 회원가입과 Resend 이메일 서비스를 연동하면서 겪은 문제들을 정리한다. 1. 역할별 조건부 다단계 회원가입 폼 요구사항 사용자 역할이 두 종류인 서비스에서 회원가입 플로우를 다르게 가져가야 했다. 역할 A: 기본 정보 → 업무 선택 → 소속 정보 (3단계) 역할 B: 기본 정보 → 업무 선택 (2단계, 소속 정보 불필요) Svelte 5 Runes로 조건부 스텝 구현 $derived로 역할에 따라 전체 스텝 수와 버튼 동작을 동적으로 처리했다. ...

2025-12-20 00:00 · 4분 소요 · Seunghan
Rails Denormalized Cache Vs Count Query

Rails 비정규화 캐시 컬럼과 COUNT 쿼리 불일치: 씨드 데이터가 0%를 만들었을 때

Rails 앱에 데모용 씨드 데이터를 직접 삽입했는데, 화면에서 모든 퍼센트가 0% 로 표시되는 상황을 만났다. 서버 로그도 깨끗하고, 데이터는 DB에 분명히 들어가 있는데, 숫자만 안 나온다. 상황 투표 기능이 있는 Rails 앱이다. 선택지(Choice)마다 득표 수를 보여주는 화면이 있고, 전체 투표수 대비 퍼센트를 계산해서 프로그레스 바와 숫자로 표시한다. 데모를 보여줘야 해서 외부 API에서 실시간 데이터를 가져와 씨드 데이터로 넣었다. 방식은 간단했다. # 씨드 데이터: 컬럼을 직접 업데이트 choice.update_column(:vote_count, 4712) pick.update_column(:total_votes, 6536) DB를 직접 조회하면 숫자가 잘 들어가 있다. 그런데 화면에서는: ...

2025-12-16 00:00 · 4분 소요 · Seunghan
Crypto Exchange Api Integration Lessons

Binance·Bybit·OKX 5개 거래소 API — 공식 문서가 틀렸을 때 Rails에서 살아남기

Ruby on Rails로 여러 암호화폐 거래소의 펀딩레이트(funding rate)를 수집하는 기능을 만들면서 겪은 문제들을 정리한다. 5개 거래소를 붙이면서 각 거래소마다 API 동작 방식이 달랐고, 공식 문서와 실제 동작이 다른 경우도 있었다. 거래소 API의 공통 기반 클라이언트 만들기 여러 거래소를 붙이기 전에 공통 HTTP 클라이언트를 먼저 만들었다. Faraday를 사용했고, 재시도와 Circuit Breaker를 여기에 몰아 넣었다. Faraday + faraday-retry 설정 # Gemfile gem "faraday" gem "faraday-retry" def connection @connection ||= Faraday.new(url: base_url) do |f| f.request :retry, { max: 3, interval: 0.5, backoff_factor: 2, interval_randomness: 0.5, # jitter retry_statuses: [429, 503, 504], retry_block: -> (env, options, retries, exc) { Rails.logger.warn("[#{exchange_name}] Retrying... #{retries} left. Status: #{env.status}") } } f.adapter Faraday.default_adapter f.options.timeout = 10 f.options.open_timeout = 5 end end backoff_factor: 2와 interval_randomness: 0.5(jitter)를 조합하면 재시도 간격이 0.5초 → 1초 → 2초로 지수 증가하면서 약간의 무작위성이 붙는다. 거래소 API가 Rate Limit(429)을 돌려줄 때 모든 클라이언트가 동시에 재시도하는 “thundering herd” 문제를 막아준다. ...

2025-12-06 00:00 · 6분 소요 · Seunghan
Api Response Wrapper Token Parsing Debug

로그인이 자꾸 풀린다 — API 래퍼 포맷 불일치가 만든 연쇄 버그

모바일 앱에서 로그인이 자꾸 풀린다. 로그인 직후는 정상인데, 앱을 잠깐 백그라운드로 내렸다가 다시 열면 로그인 화면이 뜬다. SecureStorage에 토큰 저장도 확인했고, Dio 인터셉터로 401 자동 갱신도 구현되어 있는데 왜? 증상 재현 앱 로그인 → 정상 동작 액세스 토큰 만료 시점 전후로 앱 재시작 → 세션 복원 실패, 강제 로그아웃 서버 로그에서 힌트를 찾았다. FormatException: "user" field is missing or null 토큰 갱신 응답을 파싱하다가 터지고 있었다. 구조 파악 서버는 모든 API 응답을 공통 래퍼로 감싼다. ...

2025-12-02 00:00 · 4분 소요 · Seunghan
Spa Blank Screen Inertia Usepage Url Debugging

SPA 배포 후 빈 화면: Inertia.js usePage().url은 string이다

Rails + Inertia.js + Svelte 앱을 배포한 뒤 접속하면 완전히 빈 화면만 보였다. 서버는 정상이고 에셋도 다 로드되는데 화면이 안 그려지는 상황. 원인 추적부터 해결까지, 그리고 재발 방지를 위한 패턴까지 정리한다. 증상 배포된 URL 접속 시 빈 화면 (흰색 배경만 표시) 로컬 개발 서버에서는 정상 동작 아무런 에러 페이지 없이 그냥 빈 화면 서버 로그에도 이상한 점 없음 (200 응답, 정상적인 요청 처리) 이 상황이 특히 짜증스러운 이유는, 서버 입장에서는 완전히 정상 동작하고 있기 때문이다. HTTP 상태 코드도 200, 에러 로그도 없다. 문제는 브라우저 안에서만 발생한다. ...

2025-11-22 00:00 · 6분 소요 · Seunghan
Rails Inertia Svelte Pet Avatar Image Color

Rails + Inertia + Svelte 5: 아바타 이미지/색상 선택 기능 구현에서 삽질한 것들

Rails 8 + Inertia.js + Svelte 5 스택으로 펫(반려동물) 프로필 아바타를 이미지 또는 색상으로 선택하는 기능을 구현하면서 겪은 문제들을 정리한다. 문제 1: 색상이 DB에 저장되지 않았다 증상 처음 코드를 보니 펫 카드에 색상을 표시할 때 이런 식으로 되어 있었다. const PET_COLORS = ['#f3caa1', '#b7ddf9', '#d3c8ff', '#c5d5f4', '#ffd9aa'] function petColor(index: number): string { return PET_COLORS[index % PET_COLORS.length] } 펫을 생성한 순서(인덱스) 로 색상을 결정하는 구조였다. 색상을 DB에 아예 저장하지 않았으니, 사용자가 색상을 바꿔도 새로고침하면 원래 색상으로 돌아왔다. 해결 마이그레이션으로 avatar_color 컬럼을 추가하고 기본값을 지정했다. ...

2025-11-15 00:00 · 4분 소요 · Seunghan
Rails8 Deploy Lessons

Rails 8 첫 배포에서 마주친 5가지 문제: 보안, 마이그레이션, 호환성

Rails 8 프로젝트를 처음 클라우드 서비스에 배포하면서 하루 동안 연속으로 5가지 문제를 만났다. 각각 독립적인 문제처럼 보였지만, 하나를 고치면 다음 문제가 드러나는 패턴이었다. 특히 Rails 8에서 새로 도입된 Solid Suite의 멀티 DB 구조는 기존 Rails 개발자에게도 낯선 부분이 많았다. 시간 순서대로 마주친 문제와 해결 과정을 기록한다. 1. 공개 저장소에 민감한 파일이 들어간 경우 증상 배포 전 보안 점검을 하다가 git 히스토리에 민감한 파일이 포함된 것을 발견했다. 현재 HEAD에는 없더라도 과거 커밋에 남아있으면 누구나 조회할 수 있다. ...

2025-11-11 00:00 · 6분 소요 · Seunghan
Apple Sso 403 Email Verified Type Mismatch

Apple Sign-In 403 에러: email_verified 타입 불일치와 복붙 버그 3종 세트

Apple Sign-In이 403 Forbidden으로 실패하는데, Google Sign-In은 정상 동작하는 상황이었다. 동일한 스택(Rails 8 + Flutter)의 다른 프로젝트에서는 Apple 로그인이 잘 되고 있어서 비교 분석했다. 결론부터 말하면, 세 가지 독립적인 버그가 동시에 존재했고, 모두 “Google SSO 코드를 복붙해서 Apple SSO를 만든” 과정에서 생겨났다. 증상 Apple 로그인: 403 Forbidden Google 로그인: 정상 성공 에러 메시지: "Email not verified by Apple" 개발 환경에서는 재현되지 않고 프로덕션에서만 발생 (Apple 테스트 계정 이슈) 배경: Apple과 Google JWT는 다르다 OAuth 2.0 / OIDC 표준은 email_verified 필드가 boolean이어야 한다고 명시하고 있다. 하지만 현실에서 Apple은 이 필드를 문자열 "true"로 반환하는 경우가 있다. 이건 Apple의 공식 문서에도 명확히 나와 있지 않은 엣지 케이스다. ...

2025-10-25 00:00 · 6분 소요 · Seunghan
Rails Missing Migration Sessions Table

Production DB에 테이블이 없다: schema.rb와 migration 파일 불일치 사고

회원가입, 로그인이 전부 안 된다는 제보를 받았다. 앱에서는 “예상하지 못한 오류가 발생했습니다"만 반복. 증상 회원가입 시도 → 500 Internal Server Error 로그인 시도 → 동일하게 500 Health check API → 200 OK, DB 연결 정상 서버는 살아있고 DB도 연결되어 있는데, 인증 관련 기능만 전멸. 조사 과정 1단계: 서버 상태 확인 SSH로 접속해서 Rails 환경 확인. rails runner "puts Rails.env" # => production rails runner "puts User.count" # => 13 서버 정상, DB 연결 정상, 유저 데이터도 존재. ...

2025-10-18 00:00 · 4분 소요 · Seunghan
Rails Flutter Server Health Check 4 Issues

Rails + Flutter 앱 서버 점검기: 한 번에 터진 4가지 문제와 해결

앱 테스트 빌드를 올리고 직접 돌려보니 한꺼번에 4가지가 안 됐다. Google 로그인 실패, AI 일정 생성이 엉뚱한 결과, 알림 버튼 누르면 크래시, 인기 여행지 섹션이 텅 비어있음. 하나씩 원인을 찾고 고친 과정을 정리한다. 1. Google SSO는 실패하는데 Apple 로그인은 성공 증상 Apple Sign-In은 정상 동작하지만 Google Sign-In만 500 에러. 클라이언트에서는 로그인 실패 토스트만 보인다. 원인 컨트롤러는 이전 커밋에서 수정했지만, Model의 from_omniauth 메서드는 그대로였다. # User 모델 — 마이그레이션 후에도 옛날 컬럼명 참조 def self.from_omniauth(auth) user = find_or_initialize_by(provider: auth.provider, uid: auth.uid) # uid 컬럼 없음 user.image = auth.info.image # image 컬럼도 없음 end DB 스키마에서는 uid → provider_uid, image → avatar_url로 마이그레이션된 상태. 컨트롤러 쿼리는 수정했지만 모델 내부 메서드가 여전히 옛 컬럼을 참조하고 있었다. ...

2025-10-15 00:00 · 5분 소요 · Seunghan
Flutter Sync Queue Aggressive Error Handling

Flutter Sync Queue에서 불필요한 에러가 반복 노출되는 문제 해결

모바일 앱에서 오프라인 동기화를 위해 Transactional Outbox 패턴을 구현하던 중, 동기화가 실제로는 정상 완료되었는데도 “동기화 실패” 에러가 반복적으로 사용자에게 노출되는 문제를 발견했다. 현상 앱에서 다음과 같은 에러가 반복적으로 발생했다: AppException: Failed to push changes: AppException: Push completed with failures; retry count: 2, pending changes remain in queue. 서버 로그를 확인하면 동기화 pull은 정상 동작하고, 실제 데이터도 이미 동기화된 상태였다. 구조 파악: Transactional Outbox 패턴 앱의 동기화 구조는 다음과 같다: ┌────────────────┐ ┌──────────────┐ ┌────────────────┐ │ Local DB │────▶│ Sync Queue │────▶│ Remote API │ │ (Drift/SQLite)│ │ (Outbox) │ │ (Rails) │ └────────────────┘ └──────────────┘ └────────────────┘ 로컬에서 데이터 변경 → sync queue에 pending 아이템 추가 performFullSync() 호출 시 pull → push 순서로 동기화 push 단계에서 queue의 각 아이템을 서버에 전송 성공하면 queue에서 제거, 실패하면 retry count 증가 원인 분석 _pushChanges() 메서드의 에러 처리 로직에 문제가 있었다: ...

2025-10-04 00:00 · 4분 소요 · Seunghan
Flutter Rails Auth Session Persistence Debugging

Flutter + Rails 인증 세션이 계속 풀리는 문제 - 3가지 원인과 해결

Flutter BLoC 앱에서 로그인을 해도 세션이 자꾸 풀린다. 분명 SecureStorage에 토큰도 저장하고, Dio 인터셉터로 401 시 자동 갱신도 구현했는데 왜? 서버 로그부터 시작해서 원인 3개를 찾고 모두 고친 과정을 정리한다. 기술 스택 모바일: Flutter + BLoC 패턴 + Dio HTTP + SecureStorage 서버: Rails 8 API + ActionCable WebSocket 인증: SHA-256 digest 기반 access token + JTI refresh token (90일) 실시간: ActionCable WebSocket (토큰 기반 인증) 증상 로그인 직후는 정상 동작 시간이 지나면 API 요청이 401로 실패 토큰 갱신은 되는 것 같은데 WebSocket이 끊어짐 결국 앱이 미인증 상태로 전환 원인 1: 레거시 코드의 유령 - DTA 잔존 메서드 발견 서버 로그에서 토큰 갱신 시 user.tokens 관련 에러가 간헐적으로 보였다. 이전에 devise_token_auth(DTA)를 사용하다가 자체 토큰 시스템으로 마이그레이션했는데, token_refresh_service.rb에 DTA 시절 코드가 남아 있었다. ...

2025-09-27 00:00 · 4분 소요 · Seunghan
Telegram Bot Intent Classification Bugs

Telegram 봇 의도 분류 버그 3가지와 Inline Keyboard 확인 플로우 구현

Telegram 봇에 자연어로 할 일을 추가하는 기능을 운영하던 중 발생한 버그 3가지와, 사용자 경험 개선을 위한 inline keyboard 확인 플로우 구현 내용을 정리한다. 이 봇의 기본 동작 방식은 다음과 같다. 사용자가 자유 형식 문자열을 입력하면, Rails 백엔드가 먼저 정규식 기반 사전 필터를 거쳐 의도를 추측하고, 이후 Gemini AI를 호출해 최종 intent와 파라미터(날짜, 시간, 내용 등)를 추출한다. 이 2단계 구조에서 각 단계에 버그가 하나씩 숨어 있었고, 그 결과가 예상치 못한 방식으로 결합되어 사용자가 의도하지 않은 동작을 일으켰다. ...

2025-06-25 00:00 · 9분 소요 · Seunghan
개인정보처리방침 이용약관 면책조항 문의