본문으로 건너뛰기

"글쓰기" 태그로 연결된 5개 게시물개의 게시물이 있습니다.

모든 태그 보기

🌱 26년 2월 회고

약 18분
Ju young Lee
A contribution-driven developer

들어가며

2월을 돌아보며, 설날 연휴에 터진 결제 장애부터, 브라우저 간 인증 연동 버그까지 예상치 못한 문제들을 만났고, 그때마다 의도적으로 조금 더 깊이 파악해보는 노력을 했다.

AI 덕분에 본질을 빠르게 파악할 수 있어서 유용했지만, 문제의 본질이라고 생각한 게 본질이 아닐 수 있기에 계속 공부해야 함을 느낀 한 달이었다.

돌이켜보면, 그 과정에서의 고민들이 2월의 가장 큰 자산이 된 것 같다. 2월에는 크게 세 가지 사건들이 있었다.

  1. 설날 결제 장애 대응 — 사파리에서 결제 완료 페이지가 흰 화면으로 뜨는 문제를 긴급 대응
  2. 웹-지도 인증 연동 트러블슈팅 — 사파리/크롬에서 회원가입 후 지도에 인증이 반영되지 않던 문제
  3. UI 제안으로 문의율 감소 — 개발자가 먼저 고객 불편을 발견하고, 작은 UI 변경으로 웹 문의를 감소시킨 경험

먼저 3가지 문제를 더 쉽게 이해하기 위해 오픈닥터웹의 현재 상황을 알아보고 3가지의 문제 및 해결 과정을 살펴보고 마무리하고자 한다.


오픈 닥터 웹의 현재 상황

오픈닥터 웹은 Flutter-web에서 Next.js로 마이그레이션 중에 있다. 그래서 발생한 문제들이었다.

현재 구조를 단순화하면 이렇다.

  1. Next.js가 부모 iframe으로 바깥을 감싸고 있다.
  2. Next.js의 자식 iframe으로 Flutter-web이 들어가 있다.
  3. Flutter-web의 자식 iframe으로 React 컴포넌트가 들어가 있다.

img alt="migration"

원래는 Flutter-web <-> react 였는데 위의 이미지와 같이 Next.js <-> Flutter-web <-> react 로 변경됐다.

이 구조가 주는 이점은 SEO를 병렬적으로 챙길 수 있다는 것이었다. 도메인 별로 조금씩 Next.js로 컴포넌트를 구현하여 갈아끼우면 되는 환경을 구축할 수 있었다.

대신 어려움도 존재했다. 하나의 기능을 만들 때도 여러 가지 고려해야 할 게 많았다. 예를 들어 브라우저의 히스토리 스택 및 상호 작용이다. 무엇보다... 크로스 브라우징 이슈였다. iframe과 PostMessage API를 사용하여 이 환경을 구축했는데 Safari에선 메시지가 보내지지 않는다거나?! 그런 경우가 잦았다.

Safari는 ITP(Intelligent Tracking Prevention) 같은 정책으로 iframe 내부의 스토리지·쿠키·네비게이션을 크롬보다 훨씬 엄격하게 제어하는 것을 알았다. 똑같은 코드가 크롬에선 멀쩡히 돌다가 사파리에선 조용히 멈추는 일이 이 경계에서 생겼다... 이런 문제가 가장 해결하기 난감했고 아래에서 살펴볼 문제들의 주된 원인이 되었다.

이제 3가지 문제 및 해결 과정을 간단하게 살펴보려고 한다.


1. 설날에 갑자기 리포트 결제가 안된다는 문의가!?

먼저 오픈닥터의 리포트는 무엇인가? 살펴보고자 한다. 많은 관심 부탁드립니다!

img alt=&quot;report&quot;

오픈닥터 리포트 서비스는 사용자가 알길 원하는 입지의 의원 정보를 조합해 개원에 도움이 되는 인사이트를 제공하는 오픈닥터웹의 유료 서비스다. 그런데 하필 설날 연휴에 "리포트를 결제했는데 안 보인다"는 문의가 빗발쳤다. 아무튼 긴급 대응이 필요했고, 두 가지 방식으로 대응했다.

img alt=&quot;customer-reception&quot;

  1. 결제했지만 리포트가 보이지 않는 고객은 환불 처리를 도와드렸다.
  2. 결제했지만 리포트가 보이지 않는 고객 중 반드시 리포트를 봐야만 하는 고객은 어드민 권한으로 오픈닥터 웹에 접근해 해당 지역 리포트를 PDF로 뽑아 메일로 직접 전달

해당 원인 파악

해당 문제는 리포트 상세에서 마이그레이션 이후 발생한 문제였다. 마이그레이션의 부작용이라고 볼 수도 있는데 이런 부분이 어려웠다... 아무리 테스트 코드가 있어도 커버하기 쉽지 않았다. 테스트 케이스를 꼼꼼히 작성하지 않았던 내가 문제였을 수 있고 테스트 코드로 외부 SDK를 검증하는게 어려운 것일 수 있겠다는 생각이 든다.

변경된 UI는 아래와 같다.

  • ASIS (Flutter-web 리포트 상세) img alt=&quot;m-report-asis&quot;
  • TOBE (Next.js 리포트 상세) img alt=&quot;m-report-tobe&quot;

급한 마음에 우선 아래의 한 줄을 플러터웹 코드에서 추가해서 강제로 해당 결제 완료 페이지로 반드시 이동하게 했다.

window.parent.postMessage({ type: "paymentResult", ... }, "*");
window.location.href = uri; // 강제로 페이지 이동하는 로직을 플러터에서 추가! 기존에는 Next.js에서 페이지 이동 처리를 진행

그런데 회고를 작성하면서 커밋 히스토리를 다시 꺼내보니, 구조적으로 페이지를 이동시키는 주체의 변경으로 해결한 문제였다.

  • 자식(Flutter iamport.js): 결제 성공 → window.parent.postMessage로 부모에 결과만 전달.
  • 부모(Next.js): 메시지 수신 → router.push('/payment/complete?...')로 결제 완료 페이지 네비게이션

페이지 이동 로직의 책임은 부모(Next.js)가 갖고 있었는데, 자식이었던 플러터웹에서도 결제 부분에선 이동할 수 있도록 했다.

Flutter 측 iamport.js — 자식이 스스로 이동하는 로직 추가:

window.parent.postMessage({ type: "paymentResult", ... }, "*");
// Flutter 결제 완료 페이지로 이동 (WebTabIamportResult → POST /report/region/payment 호출)
window.location.href = uri;

Next.js 측 오버레이 훅 — 부모가 하던 네비게이션 로직 제거:

- router.push(`/payment/complete?${query.toString()}`);
- router.refresh();
+ // Flutter가 iamport.js에서 직접 /payment/complete 페이지로 이동하므로
+ // Next.js는 결제 모달 오버레이만 닫아줌

조금 더 제대로 이해하기 위해선 서버가 "결제됐다"를 알게 되는 타이밍을 봐야 한다.

이렇게 해결할 수 있었던 이유는 서버에서 결제 완료를 알아차리는 타이밍이 Flutter 쪽 WebTabIamportResult 위젯이 화면에 마운트될 때 Riverpod (플러터의 전역 상태관리 라이브러리, zustand과 같다) 의 postReportPaymentProvider가 사용되고, 여기서 POST /report/region/payment가 호출된다.

즉 서버가 결제 완료를 알게 되는 시점이 Flutter 위젯이 화면에 마운트되는 순간에 일어난다. 위젯이 마운트되지 않으면 서버는 결제 사실을 모르고, 고객 입장에서는 돈은 나갔는데 리포트가 안 열리는 상황이 된다.


2. 오픈닥터 웹에서 인증하면 지도에 반영이 안 된다?

뉴오픈닥터(Next.js)에 회원가입 기능을 배포한 뒤, 며칠 후 발견된 문제였다. Next.js에서 회원가입/로그인을 하면 웹은 정상인데, Flutter-web 지도에는 인증 상태가 반영되지 않았다. 분명 QA를 했는데 놓쳤다... 아쉬운 마음이 컸다.

부모-자식 사이 인증 상태 동기화가 빠져 있었다.

Next.js(부모)에서 로그인이 일어나도, Flutter-web(자식 iframe)이 그 사실을 알아야 지도가 인증 상태로 동작한다. 이 로직이 분명히 있긴 했는데 제대로 동작하지 않았다.

해당 문제의 원인을 도식화 해보았다.

img alt=&quot;m-auth-token-error&quot;

근본 원인: Safari iframe에서 FlutterSecureStorage가 멈추는 게 원인이었다.

디버깅 과정에서 알게 된 원인은 이랬다.

  1. Next.js에서 회원가입 → postMessage로 Flutter-web에 토큰 전달
  2. Flutter-web이 토큰을 FlutterSecureStorage에 저장 시도
  3. FlutterSecureStorage의 웹 구현체는 내부적으로 WebCrypto + IndexedDB를 사용
  4. Safari의 ITP가 iframe 내 third-party IndexedDB 접근을 파티셔닝 처리
  5. crypto.subtle 접근이 차단되면 Promise가 resolve/reject 없이 무한 대기(hang)
  6. 결과: 토큰 저장 실패 → API 호출 시 인증 헤더 없음 → 지도에 인증 상태 미반영

크롬은 iframe 내 third-party storage 접근을 Safari만큼 엄격히 제한하지 않기 때문에, 크롬에서는 문제가 재현되지 않았다.

해결: FlutterSecureStorage → 인메모리 토큰으로 전환

img alt=&quot;m-auth-token-solve&quot;

모든 곳에서 FlutterSecureStorage 직접 읽기를 제거하고, AuthManager().opn_token에 인메모리로 저장/읽기하도록 변경했다.

// Before: Safari iframe에서 hang 발생
const storage = FlutterSecureStorage();
var opnToken = await storage.read(key: "OPN_OPNDOCTOR_KEY_AUTH_OPN_TOKEN");

// After: 인메모리 토큰 사용 (Safari iframe에서 FlutterSecureStorage가 멈출 수 있어 사용하지 않음)
var opnToken = AuthManager().opn_token;

동시에 Next.js 쪽에서도 Flutter ↔ Next.js 간 인증 상태 양방향 동기화를 위한 메시지 핸들러(useAuthMessageHandlers)와 세션 관리 포인트를 한 곳에 모아 관리하도록 했다.


3. 간단한 UI/UX 개선 시도

고객 입장에서 불편한 지점을 발견하면, 사소하더라도 제안하고 고치는 태도가 중요하다고 생각한다. 제품팀 리더인 성욱님은 마음껏 팀 내에 새로운 기능 및 UI/UX 관련해서 제보할 수 있는 분위기를 만들어주셔서 너무 감사하다.

2월에는 2가지 UI 개선을 직접 제안했다. 3/4월에는 더 많이 제안해보려고 한다.

주민등록번호 안내 툴팁 한 줄의 효과

웹에서 면허 인증이 안 돼요라는 문의가 계속 들어왔다... 우리는 따로 CS팀이 없어서 내가 직접 대응하곤 하는데 그 원인을 파악해보니 주민등록번호를 잘못 입력할 경우 면허 인증에 문제가 생긴다는 것을 발견했다.

img alt=&quot;error-signup&quot;

해결 방법은 두 가지로 나눠 진행했다.

  1. 근본 해결: 면허 인증 API 제공 외주 업체에 예외처리 수정을 요청
  2. 즉시 조치: 주민등록번호 입력란에 올바른 형식을 안내하는 툴팁 추가 — 디자이너와 논의하여 바로 반영

img alt=&quot;add-signup&quot;

카카오톡 문의 비교 (툴팁 적용 전 vs 후)

효과를 공정하게 보려면 같은 조건에서 비교해야 한다. 오픈닥터 웹의 회원가입 기능이 프로덕션에 배포된 건 1월 19일, 툴팁이 반영된 건 2월 13일이다. 이 두 시점을 기준으로 카카오톡 채널 상담 데이터를 잘라봤다.

구간기간관련 문의 건수
툴팁 적용 전1/19 ~ 2/12 (25일)7건
툴팁 적용 후2/13 ~ 3/9 (25일)4건

같은 25일 기준으로 놓고 보면 약 43% 감소했다. 절대 건수가 크진 않지만, 외주 업체의 근본 수정이 들어오기 전 임시 조치로서는 유의미한 감소였다고 본다.

문의 집계 시 주의: 키워드("면허 인증", "주민등록번호" 등)로 관련 문의만 추렸고, 설 연휴 기간(2/8~2/10)은 결제 장애 문의가 겹쳐 있어 분석 시 염두에 뒀다.

결과적으로 3월에는 면허 인증 관련해서 문의가 들어오지 않게 됐고 대응 시간을 아낄 수 있게 됐다.

매물 목록 빈 상태 개선

알게 된 친구에게 오픈닥터를 보여줄 기회가 있었다. 그런데 알고 보니 친구 아버지가 의사셔서 함께 서비스를 둘러보게 됐다. 그 상황도 지금 생각해보면 웃긴 상황이다. 그런데 지도에서 특정 위치로 이동했을 때 "에러가 있는 것 같다"고 말씀하셨다... 듣고 식겁했었다.

약간 경직된 상태로 다시 살펴보니 에러는 아니었다. 해당 위치에 매물이 없어서 "전체 매물 0"이 표시된 것뿐이었다. 기능적으로는 문제는 없지만, 사용자 입장에서는 뭔가 잘못된 것처럼 느껴지는 화면이었다.

그래서 단순히 빈 상태 메시지를 보여주는 것에서 한 발 더 나아가, 실시간으로 매물이 가장 많은 지역이나 의원이 많은 위치를 추천해서 바로 이동할 수 있는 기능을 제안했고, 반영되어 구현했다.

img alt=&quot;no-property&quot;

백엔드 API도 만들어보았다. 로직은 매물 및 의원이 가장 많은 동 10개를 반환하는 API였다. 해당 API와 프론트 UI를 엮어 구현해보았다.


마무리하며

2월에 만난 장애들은 모두 iframe 안에서 부모-자식 사이의 통신에 따른 크로스 브라우징 이슈로 발생했고, 어렴풋이 알던 개념을 실제 문제를 만나 알게 된 한 달이었다. UI 개선 경험은 코드 바깥에서도 작은 변화를 만들어낼 수 있다는 걸 확인하게 해줬다. 회고가 4월까지 밀렸지만, 그만큼 시간을 두고 다시 보니 더 또렷이 남는 사건들이다. 3월에는 오픈닥터 웹 마이그레이션을 어떻게 마무리지었는지 이어서 정리해보려 한다.


어떤 기여를 해왔을까?