[PonsLink] 방은 있었지만 제품을 다시 지어야 했다
겨울에 다시 지어야 했던 건 UI가 낡아서가 아니었다. 방을 열 수는 있었지만 권한, 결제, 요청, 기록, 배포가 따로 움직이면 운영 가능한 제품이라고 부르기 어려웠다. 이 판단은 ponslink-room-frontend/src/pages/RequestAction.tsx와 ponslink-room-frontend/src/pages/SessionAccess.tsx를 함께 볼 때 더 분명해진다. 화면에서 보이는 기능은 결과이고, 실제 제품성은 그 기능이 열리는 조건과 실패했을 때 돌아갈 길에 있다. 그래서 용어 사전 대신, 그 날 무엇을 보고 무엇을 바꿨는지부터 적는다. 같은 단어를 쓰더라도 제품 문맥이 다르면 답이 갈렸다. 그 갈림길을 숨기지 않는 쪽으로 썼다.

제품을 다시 지어야 했던 지점
방은 있었지만 제품을 다시 지어야 했다에서 먼저 확인한 파일은 ponslink-room-frontend/src/pages/RequestAction.tsx이다. 이 근거는 글의 출발점이 추상적인 제품 감상이 아니라 실제 화면과 상태를 가진 코드였음을 보여 준다. 화면 컴포넌트가 어떤 이름으로 나뉘어 있는지 따라가면 사용자가 보는 흐름과 운영자가 책임지는 흐름이 어디서 갈라지는지 보인다. 그 갈라짐을 문장으로 남기지 않으면 글은 다시 기능 소개로 돌아간다.
routes.ts의 session-access와 request-actions 경계는 재빌드의 핵심 근거였다. action token은 한 번 쓰이면 다시 쓰면 안 되고, reservation join link는 맞는 access token이 있어야 한다. 결제 성공도 provider가 말해 준 사실일 뿐, 제품 내부 예약 상태가 열렸다는 뜻은 아니다. 이 작은 차이를 지키지 않으면 브라우저 return page가 권한을 너무 쉽게 열어 버린다. 이 경험 때문에 이후에는 새로운 기능을 붙일 때마다 먼저 상태 전이를 그렸다. 성공 화면부터 만들면 빠르지만 실패 화면이 비게 된다.
실패 화면이 비면 운영자는 사용자를 직접 달래야 한다. PonsLink가 제품이 되려면 코드가 좋은 것보다 사용자가 기다리는 이유를 정확히 말할 수 있어야 했다. paid consultation gate는 운영자가 가장 실수하기 쉬운 부분이었다. 결제 provider, webhook, reservation, session token이 서로 다른 속도로 움직인다. 사용자는 한 화면에서 이것을 보지만 시스템은 네 개의 사건으로 처리한다. 재빌드는 이 사건들을 억지로 합치지 않고 순서를 보존하는 쪽으로 갔다.
그 결과 코드는 더 장황해졌지만, 문제가 생겼을 때 어디에서 멈췄는지 설명할 수 있게 됐다.

현재 구성의 가정
결제는 특히 위험했다. 결제 provider가 성공 화면을 보여 줬다고 해서 제품 안의 예약이 열린 것은 아니다. webhook 검증이 끝나고 내부 상태가 바뀌어야 한다. 이 차이를 지키는 코드는 귀찮지만, 돈을 받은 뒤 권한을 잘못 여는 사고를 막아 준다. 운영 관점에서는 ponslink-api-infra/src/http/routes.ts가 글의 방어선이다. 사용자는 방을 하나의 경험으로 보지만 시스템은 요청, 세션, 미디어, 파일, 결제, 기록처럼 여러 책임으로 나뉜다.
이 책임을 나눠 적으면 글은 조금 덜 매끈해지지만, 실패했을 때 어디서 멈춰야 하는지 설명할 수 있다. 이번 배치에서 모든 글에 실패나 한계를 넣은 것도 이 때문이다. SessionAccess.tsx는 게스트 입장에서 같은 문제를 보여 준다. 사용자는 링크를 눌렀고, 결제를 했고, 이제 들어가면 된다고 기대한다. 하지만 reservation access token이 맞지 않거나 payment webhook이 아직 도착하지 않았으면 제품은 입장을 막아야 한다.
이때 막는 이유를 설명하지 못하면 보안은 사용자에게 장애처럼 보인다. 그래서 대기와 거절 상태를 명시적으로 보여 주는 일이 중요했다. 초기에는 방을 중심에 놓고 주변 기능을 붙였다. 요청은 요청 화면에서, 결제는 별도 provider에서, 세션 접근은 또 다른 링크에서 처리했다. 작은 실험에서는 이 정도 분리가 편했지만 운영에서는 상태가 어긋날 여지가 많았다.
이 선택은 빠르게 확인하기 좋았다. 사용자는 들어오고, 장치는 켜지고, 메시지는 오간다. 하지만 빠른 확인이 곧 운영 가능한 구조라는 뜻은 아니었다. 권한이 화면 안에 섞이면 나중에 기능을 줄이기도 어렵고, 문제가 생겼을 때 어디를 막아야 하는지도 흐려진다. 프론트엔드에서는 RequestAction과 SessionAccess가 이 경계를 사용자 언어로 보여 준다. 사용자는 결제를 마친 것처럼 느껴도 시스템은 webhook이 들어오고 예약이 열릴 때까지 대기 상태를 보여야 한다.
운영자는 이 지연을 오류처럼 숨기면 안 된다. 사용자는 이 복잡도를 몰라도 된다. 대신 제품은 사용자가 보지 않는 곳에서 무엇을 허용하고 무엇을 미룰지 결정해야 한다. 그 결정을 숨기면 당장은 매끄러워 보여도 장애나 결제 지연, 네트워크 실패가 왔을 때 설명할 말이 없어진다.

재건이 선명해진 순간
처음 버전은 만들면서 배운 흔적이 많았다. 방 화면은 있었고 링크도 있었고 WebRTC 연결도 됐다. 하지만 제품으로 팔려면 호스트가 요청을 받고, 유료 상담을 제안하고, 결제 상태에 따라 입장을 열고, 끝난 뒤 기록을 남기는 흐름이 끊기면 안 됐다. 그래서 기능 소개가 아니라 그때 내가 어떤 기준을 세우지 못했고, 어떤 실패를 보고 나서 경계를 다시 잡았는지를 따라간다.
PonsLink는 회의 앱을 흉내 낸 프로젝트가 아니라 요청에서 방, 방에서 기록으로 이어지는 흐름을 제품으로 만들려는 시도였다. 이어 읽으면 좋은 글은 [과 [이다. 앞 글은 왜 이 문제가 생겼는지 보여 주고, 다음 글은 같은 기준이 다른 기능이나 운영 경계로 어떻게 넘어갔는지 보여 준다. 재빌드는 그래서 UI 갈아엎기가 아니라 상태 전이 재정의였다.
request accepted, payment_pending, paid, scheduled, access granted 같은 이름이 분명해야 운영자가 문제를 찾을 수 있다. 상태 이름이 흐리면 로그를 봐도 어디서 멈췄는지 알 수 없다. 다시 지은 구조는 request, payment, reservation, session access를 명시적으로 나눴다. API README의 paid consultation gate처럼 checkout 생성, payment_pending, webhook paid, reservation scheduled, session access unlock이 순서대로 이어져야 한다. 이때부터 PonsLink의 중심은 단순한 room component가 아니라 상태와 권한의 연결이 되었다. ponslink-api-infra/src/http/routes.ts와 ponslink-api-infra/README.md를 보면 이 판단이 프론트엔드 취향이 아니라 API/BFF 경계와 맞물린 결정이라는 점이 보인다.
마지막으로 ponslink-api-infra/README.md를 함께 읽으면 다음 작업의 기준이 보인다. PonsLink의 각 글은 독립된 기능 홍보가 아니라 앞뒤 글과 이어지는 판단 기록이어야 한다. 그래서 내부 링크를 두 개씩 남겼고, 이미지도 cover가 아니라 본문 근거로 다시 만들었다. 글 하나를 읽고 끝나는 것이 아니라 독자가 다음 방, 다음 권한, 다음 운영 문제로 넘어갈 수 있게 하는 것이 이번 확장의 목적이다.
*로컬 frontend의 screened request 마케팅 자산을 재가공해 요청-세션 경계를 설명했다.
분리·중계·직접연결 선택
RequestAction.tsx에서 호스트가 하는 일은 단순히 수락 버튼을 누르는 것이 아니다. 요청을 받아도 바로 방을 열지 않을 수 있고, 시간을 제안할 수 있으며, 유료 상담이면 결제 대기 상태를 거쳐야 한다. 이 차이를 화면이 숨기면 사용자는 무엇을 기다리는지 모른다. 겨울 재빌드에서 가장 먼저 바꿔야 했던 것은 버튼 모양이 아니라 상태 이름이었다. SessionAccess의 역할은 여기서 단순 입장 페이지가 아니다.
사용자가 왜 기다리는지, 왜 아직 못 들어가는지, 어떤 토큰이 필요한지를 말해 주는 번역기다. 보안 경계가 사용자 언어로 바뀌지 않으면 제품은 안전하지만 불친절해진다. 결제 성공 이후에도 webhook과 access token 검증을 기다리는 흐름. 지금 보면 재빌드는 코드를 새로 쓰는 일이 아니라 상태 이름을 다시 붙이는 일이었다. 방을 만드는 기술보다 방에 들어갈 자격을 설명하는 기술이 더 중요했다.
이 판단 덕분에 이후 결제와 권한을 한 덩어리로 다루지 않을 수 있었다. 다음에 같은 문제를 다시 푼다면 먼저 기능 목록을 쓰지 않고, 방의 목적과 닫혀야 하는 실패 경로를 표로 만들 것이다. 기능은 그 뒤에 붙여도 늦지 않다.
다시 짜 본 구조
재빌드 전에는 성공 경로가 너무 빨리 이어졌다. 요청을 받고, 결제가 되고, 방에 들어가는 흐름을 한 줄로 보면 간단하다. 하지만 실제 운영에서는 각각 다른 시스템이 다른 속도로 응답한다. 그 차이를 무시하면 가장 중요한 순간에 사용자를 잘못 안내한다. 이 글을 쓰며 남긴 가장 큰 교훈은 느린 상태를 화면에서 숨기지 말자는 것이다.
기다림을 숨기면 오류처럼 보이고, 오류처럼 보이면 사용자는 다시 문의한다. 대기 상태를 제대로 보여 주는 것도 제품 기능이다. 가장 위험한 한계는 결제 성공과 입장 권한을 같은 사건처럼 취급하는 유혹이었다. 결제 provider가 성공을 알려도 webhook 검증, reservation 상태, session access token이 맞지 않으면 브라우저가 방에 들어가면 안 된다. 이 경계를 대충 넘기면 제품은 돈과 권한을 섞어 버린다.
이 부분은 실제로 실패와 trade-off가 함께 있는 지점이었다. 기능을 열어 두면 데모는 풍성해지지만, 사용자에게 필요한 설명은 줄어들지 않는다. 반대로 권한을 좁게 잡으면 초반 경험은 답답해질 수 있지만 운영자는 사고 범위를 예측할 수 있다. 방은 있었지만 제품을 다시 지어야 했다에서 먼저 확인한 파일은 ponslink-room-frontend/src/pages/RequestAction.tsx이다. 이 근거는 글의 출발점이 추상적인 제품 감상이 아니라 실제 화면과 상태를 가진 코드였음을 보여 준다.
화면 컴포넌트가 어떤 이름으로 나뉘어 있는지 따라가면 사용자가 보는 흐름과 운영자가 책임지는 흐름이 어디서 갈라지는지 보인다. 그 갈라짐을 문장으로 남기지 않으면 글은 다시 기능 소개로 돌아간다. routes.ts의 session-access와 request-actions 경계는 재빌드의 핵심 근거였다. action token은 한 번 쓰이면 다시 쓰면 안 되고, reservation join link는 맞는 access token이 있어야 한다. 결제 성공도 provider가 말해 준 사실일 뿐, 제품 내부 예약 상태가 열렸다는 뜻은 아니다.
이 작은 차이를 지키지 않으면 브라우저 return page가 권한을 너무 쉽게 열어 버린다. '[PonsLink 방은 있었지만 제품을 다시 지어야 했다'을 관통하는 질문은 이 섹션에서도 같았고, 답만 더 구체적이 됐다.
마치며
이 글에서 확인한 경계는 다음 변경의 기준으로 남겼다.