🌱 25년 11월 회고
들어가며

이번 11월을 돌아보면 오픈닥터에서 수습이 끝난 이후, 본격적으로 프로덕트를 알게 되는 한달이었다. 레거시를 한줄 한줄 읽어보며 비즈니스 로직을 확인하는 과정이 있었다. dart로 구현된 코드이지만 언어 안에 있는 의미를 찾아가며 이해하는 과정이 재밌었다. 익숙한 환경이 아니라 더 좋았던 것 같다.
우리 프로덕트는 전격 개편을 진행하고 있다. 프로덕트 사용성 문제와 사용자 증가가 그 이유인데 이를 위해 목표하고 있는 Step은 SEO/AEO 최적화이다.
현재 오픈닥터 웹 프로덕트는 flutter를 활용하여 구현되어있다. flutter는 앱 만들 때 사용하는 프레임워크로 알고 있었으나 flutter 컴파일러에게 web으로 배포하길 원하는 플래그만 주면 알아서 웹으로 배포할 수 있게 해준다.
하지만 문제가 있다. flutter를 통해 만든 프로덕트는 극단적인 Client Side Rendering 방식으로 만들어진다. 그래서 우리가 목표하는 SEO 최적화가 어려운 상황이다.
기존 프로덕트에는 수 많은 기능들이 존재하고 숨어있는 기능들도 많은데, 달리는 마차의 바퀴를 바꾼다는게 쉽지 않을 것 같았다. 그래서 대안으로 PostMessage를 사용하여 CORS를 공식적으로 해결하는 동시에 Next.js로 SEO를 병렬적으로 잡아가는 시도를 하기로 합의했다.
마이그레이션 전략: 점진적 마이그레이션 설계
달리는 마차의 바퀴를 바꾸기 위해서는 신중한 전략이 필요했다. 기존 프로덕트를 중단 없이 운영하면서 SEO 최적화를 달성하기 위해 점진적 마이그레이션 전략을 수립했다.

기존에는 flutter-web -> react 환경이었다. flutter-web 환경을 구축한 오픈닥터 최초의 개발자가 퇴사하고 이전 개발자가 입사를 했을 때, flutter-web의 코드를 유지보수하기가 만만치 않았다고 했다. 그래서 postMessage를 활용해서 react 컴포넌트를 flutter-web 안에 새로운 Document를 가지고 와서 보여주는 방식으로 UI를 구현하시고 계셨다.
최상위에 Next.js를 두어 얻을 수 있는 이점은 다음과 같았다.
- 장점
- 기존 프로덕트 유지하며 SSR 환경 구축 가능
- 정적 및 동적 메타데이터 추가 가능
- 서버에서 데이터를 가지고 와 정적/동적 Sitemap 추가 가능
- 단점
- PostMessage를 활용하여 여러 프로그램 간 메시지 통신을 잘 관리해야 해서 마이그레이션 시 복잡성 증가
이러한 전략으로 환경을 구축하고, 도메인 설정도 변경했다.
-
as-is
- 기존 flutter-web : https://www.opndoctor.com/
- 리액트 : https://somegthingReact.opndoctor.com/
-
to-be
- 넥스트 : https://www.opndoctor.com/
- 기존 flutter-web : https://somethingOriginal.opndoctor.com/
- 리액트 : https://somegthingReact.opndoctor.com/
그리고 새벽에 배포를 시작하고 무사히 완료했다. 하지만 배포 후 예상치 못한 문제들이 기다리고 있었다.
배포 후 마주한 문제들과 해결 과정
배포는 성공했지만, 복잡한 아키텍처 변경의 여파가 바로 나타났다. 세 가지 예상치 못한 문제를 마주하게 되었다.
문제 1: 인증 토큰이 전달되지 않아 리포트가 보이지 않다.
문제 상황
배포 직후 리포트 결제 문의가 빗발쳤다. 리포트를 결제했는데, 리포트가 보여지지 않는 문제였다.
우리 서비스에는 다양한 기능들이 존재하는데 그 중에 입지 분석 리포트를 제공해주는 기능이 있다. 원하는 지역을 선택하면 해당 지역의 의원 및 약국 그리고 해당 의원/약국들의 정보를 제공하는 리포트이다.
Next.js의 서버 컴포넌트를 활용하여 구현한 리포트 페이지는 네트워크 호출도 브라우저에서 볼 수가 없어 디버깅이 어려웠다. 하지만 센트리를 통해 에러를 확인할 수 있었고, 그것은 바로 403 에러였다.
원인 분석
오픈닥터 웹 -> 리포트 페이지로 이동할 때 토큰이 옮겨지지 않은 문제였다.
오픈닥터 서비스의 특징인데 리포트는 독립적인 프로젝트로 따로 존재하고 있다. flutter-web에서 리포트 페이지로 이동할 때, 아래의 예제와 같이 쿠키를 심어주어 인증 정보를 공유하고 report가 보여지는 과정이 있다.
const domain = '.opndoctor.com';
html.document.cookie = 'token=$token; domain=$domain; path=/; secure; SameSite=Strict';
위와 같이 토큰을 쿠키에 담아 이동하는데, 마이그레이션을 하면서 도메인을 변경한 것이 문제의 원인이었다.
마이그레이션 배포 시 도메인 설정 (문제 상황):
- 넥스트 : https://www.opndoctor.com/
- 기존 flutter-web : https://**.amplify.com/
- 리액트 : https://**.opndoctor.com/
- 리포트 : https://**.opndoctor.com/
기존 flutter-web의 도메인을 https://www.opndoctor.com에서 https://**.amplify.com로 변경했는데, 이렇게 때문에 flutter-web에서 리포트 페이지로 토큰이 전달이 되지 않았던 것이다.
쿠키는 domain 속성에 따라 공유 범위가 결정된다. 코드에서 domain='.opndoctor.com'으로 설정되어 있어서, opndoctor.com과 그 모든 서브도메인(www.opndoctor.com, report.opndoctor.com 등)에서는 쿠키가 공유되지만, 완전히 다른 도메인인 *.amplify.com에서는 공유되지 않는다.
SameSite=Strict 설정도 영향을 미쳤다. SameSite=Strict는 같은 사이트에서만 쿠키를 전송하므로, 다른 도메인 간 요청 시 쿠키가 전송되지 않는다. 만약 SameSite=Lax였다면 GET 요청의 경우 다른 사이트에서도 쿠키가 전송될 수 있지만, 이 경우에도 domain 속성이 다르기 때문에 근본적으로 쿠키를 읽을 수 없는 문제는 해결되지 않는다.
해결 방법
도메인을 아래와 같이 변경하여 같은 도메인 계열로 통일했다.
- 넥스트 : https://www.opndoctor.com/
- 기존 flutter-web : https://**.opndoctor.com/
- 리액트 : https://**.opndoctor.com/
- 리포트 : https://**.opndoctor.com/
결과
인증 토큰이 무사히 넘어가게 됐고 리포트가 정상적으로 보여지게 됐다. 이 문제를 통해 도메인 변경이 인증 흐름에 미치는 영향을 깊이 이해하게 되었다.
문제 2: 리포트 재구매시 404 페이지가 보인다?
문제 상황
리포트를 재구매 결제 이후 404 페이지로 보인다는 카카오톡 문의가 쏟아졌다.
원인 분석
원인 파악해보니, 핵심 문제는 URL 렌더링 문제였는데, 이런 경우는 처음 경험해보았다. 지금까지는 이런 문제가 없었는데, Next.js를 기존 flutter-web 상위로 감싸면서 이런 문제가 발생했다. 핵심적으로는 flutter-web에서 URL이 변경됐을 때, 상위 Next.js로 해당 URL을 넘겨주는 로직에서 쿼리스트링 및 URL Hash 앞에 "?"와 "#"이 사라지는 문제였다.
해결 방법
flutter-web에서 URL이 변경될 때 URL을 제대로 빌드할 수 있는 유틸 함수를 추가하여 문제를 해결했다. 쿼리스트링과 Hash를 포함한 전체 URL을 올바르게 구성하여 상위 Next.js로 전달하도록 수정했다.
결과
리포트 재구매 후에도 정상적으로 페이지가 보이게 되었다. PostMessage를 통한 URL 동기화 로직의 중요성을 깨달았다.
문제 3: 새로고침 후 URL 동기화가 끊어진다.
문제 상황
수정한 오픈닥터 웹 아키텍처는 Next.js가 상위 HTML을 담당하고, 그 안에 iframe과 postMessage API로 Flutter-web이 하위 HTML로 삽입된다. Flutter-web 영역에서 새로고침을 하게 되면 이후에 URL이 변경되지 않는 문제가 발생했다.
원인 분석
Flutter-web에서 window.location.reload()를 직접 호출하면,
iframe 내부만 새로고침되어 부모(Next.js)와의 PostMessage 통신 연결이 끊어지게 된다.
이로 인해 이후 Flutter-web에서 URL이 변경되어도 상위 Next.js로 해당 변경사항을 전달할 수 없어, Next.js의 URL이 동기화되지 않는 문제가 발생했다.
해결 방법
Flutter-web에서 직접 새로고침하는 대신,
IframeChildManager를 통해 부모(Next.js)에게 새로고침 이벤트를 전달하도록 변경했다.
이렇게 하면 부모가 전체 페이지를 새로고침하거나 URL을 제어할 수 있어,
PostMessage 통신 채널이 유지되고 URL 동기화가 정상적으로 동작한다.
ASIS (문제 상황)
void webReload() async {
window.location.reload()
}
TOBE (해결 방법)
void webReload() async {
final manager = IframeChildManager();
const storage = FlutterSecureStorage();
final somthing_token = await storage.read(
key: "SOMETHING_TOKEN");
manager.send(
type: ParentIframeMessageType.message,
payload: {
'event': ChildIframeMessageEventType.reload.value,
'data': {
if (somthing_token != null) 'token': somthing_token,
},
},
);
}
결과
새로고침 후에도 PostMessage 통신이 유지되어 URL 동기화가 정상적으로 동작하게 되었다. iframe과 부모 간 통신 관리의 중요성을 배웠다.
문제 해결의 결과: 달성한 성과
이러한 문제들을 하나씩 해결하며 얻은 결과는 기대 이상이었다.
SEO 노출/ 클릭 상승

| 구분 | 10월(기준) | 11월 | ~12/3 | 10월 대비 11월 상승률 |
|---|---|---|---|---|
| 노출 수 | 약 1,300회 | 약 2,000회 (평균) | 약 3,700회 | 약 +180% (2.8배) 🔺 |
| 클릭 수 | 약 10회 | 약 25회 (평균) | 약 75회 | 약 +650% (7.5배) 🔺 |
개선을 시도했을 때 성과로 눈에 보이는게 너무 흥미진진한 한 달이었다.
기술적 성과
- 병렬적으로 Next.js로 각 페이지 이관 가능한 환경 구축
- 서버 컴포넌트를 활용한 SEO 최적화 기반 마련
- 점진적 마이그레이션 전략 수립 및 실행
- PostMessage를 통한 복잡한 아키텍처 관리 경험 축적
문제 해결 과정에서 얻은 교훈
11월은 몰입의 한 달이었다. 새로운 개발 환경과 프로젝트를 진행하며 심장이 뛰었던 한 달이었다.
이번 마이그레이션을 통해 배운 가장 큰 교훈은 달리는 마차의 바퀴를 바꾸는 것의 어려움이었다. 세 가지 문제를 해결하는 과정에서 다음을 깊이 느꼈다:
1. 도메인 변경이 인증 흐름에 미치는 영향
문제 1을 해결하면서 배운 점이다. 쿠키 기반 인증을 사용할 때, 도메인 간 쿠키 공유 규칙을 정확히 이해해야 한다는 것을 몸소 체감했다. domain 속성과 SameSite 설정이 인증 흐름에 미치는 영향을 이론이 아닌 실제 문제로 경험하게 되었다.
2. 서버 컴포넌트 디버깅의 어려움
브라우저에서 네트워크 호출을 볼 수 없어 센트리 같은 모니터링 도구의 중요성을 깨달았다. 서버 사이드에서 발생하는 문제를 디버깅할 때는 전통적인 방법만으로는 부족하다는 것을 배웠다.
3. 점진적 마이그레이션의 복잡성
PostMessage를 통한 여러 프로그램 간 메시지 통신 관리가 예상보다 복잡했다. 문제 2와 문제 3을 해결하면서 iframe과 부모 간 통신 관리의 미묘함을 배웠다. URL 동기화, 새로고침 처리 등 단순해 보이는 동작들이 복잡한 아키텍처에서는 예상치 못한 문제를 야기할 수 있다는 것을 알게 되었다.
마무리하며
이제 어느 정도 익숙해졌으니, 더 깊이 파서 제대로 이해해보고 제대로 수정해보자. 점진적 마이그레이션은 이제 약 20퍼센트 진행됐는데, 이제는 단위 테스트도 도입해보며 안정적이고 빠르게 변화할 수 있는 소프트웨어를 만드는 데 집중해보려고 한다.
12월은 연말로 약속이 꽤나 있다. 하지만 정신 놓치 않고 잘 25년을 마무리하고 26년을 준비하는 한 달로 긴 호흡으로 가져가려고 한다.
