[PonsWarp] 파일 전송은 PonsLink 안에서 먼저 고장났다
파일 전송을 처음부터 별도 제품으로 생각한 건 아니었다. PonsLink 방에 사람이 붙어 있으니, 파일 하나 보내는 버튼 정도는 자연스러워 보였다.
붙여 보니 먼저 무너진 것은 버튼 위치가 아니라 약속의 크기였다. 사용자는 “파일 전송” 한 줄로 이해하지만, 제품은 연결·진행률·메모리·저장 완료·실패 후 재시도를 모두 설명해야 했다.
PonsWarp는 그 설명이 방 안에서는 불가능해 보였을 때 밖으로 나온 이름이다. 이 글은 분리 선언문이 아니라, 방 안에서 전송이 어떻게 먼저 고장 났는지에 대한 기록이다.

PonsLink 안에서 고장난 지점
방 UI 안의 파일 전송은 금방 이런 질문에 부딪혔다.
- 지금 업로드 중인가, peer 전송 중인가
- 브라우저를 닫으면 이어받을 수 있는가
- 상대가 새로고침하면 진행률은 어디로 가나
- Safari/Firefox에서 저장 경로가 다르면 뭐라고 안내하나
- 전송 중 통화 품질이 떨어지면 어느 쪽을 우선하나
Chrome/Edge와 Safari의 filesystem 제약이 다르다는 사실 자체보다, 같은 버튼이 브라우저마다 다른 문장을 요구한다는 점이 치명적이었다. 회의 화면에 그 문장들을 정직하게 다 넣을수록 room은 전송 콘솔처럼 변했다.
작은 첨부 파일이면 대충 넘어갈 수 있다. 문제는 사용자가 기대하는 파일 크기가 순식간에 커진다는 점이다. 자료 묶음, 영상 샘플, 로그 압축본이 들어오면 “가벼운 공유” 가정은 끝난다. 그때부터 방 UI는 회의 도구가 아니라 미완성 전송 클라이언트처럼 보인다.
요청이 막힌다는 건 네트워크만의 문제가 아니다. 사용자가 “지금 뭐가 되고 있지?”라고 물었을 때 회의 화면이 대답할 공간이 없다는 뜻이다. 영상 타일, 참가자 목록, 채팅 입력창 사이에 전송 상태를 욱여넣으면 정보는 들어가도 이해는 안 된다. 정보 구조가 이미 거부하고 있었다.

처음에는 방 안 기능이면 충분할 줄 알았다
처음 대응은 전형적인 기능 추가였다. 재시도 버튼, 진행률 바, 용량 제한 문구, 실패 토스트.
도움이 됐지만 근본은 남았다. 통화 기능과 전송 기능의 성공 정의가 달랐다.
| 통화 | 파일 전송 | |
|---|---|---|
| 중간 끊김 | 잠시 참을 수 있음 | 조각 유실이면 실패 |
| 품질 저하 | 화질 낮추고 유지 가능 | “거의 다 옴”은 의미가 다름 |
| 종료 조건 | 상대 얼굴/음성 | 저장 완료 확인 |
| 사용자 불안 | 안 보임/안 들림 | 몇 %에서 멈춤 |
| 재시도 감각 | 자동 재연결 기대 | 어디부터 다시인지 중요 |
방 제품은 사람을 붙이는 일에 최적화되어 있고, 전송 제품은 바이트의 완결성에 최적화되어야 한다. 한 UI 상태 머신에 둘을 억지로 태우면 둘 다 어중간해진다.
더 나쁜 경우는 우선순위 충돌이다. 전송이 메모리를 밀어 올리면 영상 track이 흔들리고, 사용자는 “회의가 불안정하다”고 느낀다. 원인 파일이 전송인데 책임은 회의 제품이 진다. 이 오염 구조가 분리의 직접 동기가 됐다.
기능 추가로 증상을 덮을수록 코드 결합도도 올라갔다. 방 상태 변경 훅에 전송 재시도가 붙고, presence 이벤트에 진행률이 섞였다. 당장은 동작해도 다음 리팩터링 비용이 커진다. “버튼 하나 더”가 아키텍처 부채가 되는 전형적인 경로였다.

분리해야 한다는 게 선명해진 순간
가장 잔인했던 실패는 마지막 수 퍼센트였다. 보내는 쪽은 끝났다고 느끼고, 받는 쪽은 저장 직전에서 죽는다. 로그를 보면 연결·백프레셔·디스크/브라우저 API 중 어디에 걸렸는지 갈리는데, 방 UI는 그저 “실패” 한 줄이었다.
이 밤 이후 질문은 바뀌었다. “방에 파일을 어떻게 더 잘 붙일까”가 아니라 “전송을 방의 부가 기능으로 두면 안 되는 거 아닌가.”
실패 리포트도 바뀌어야 했다. room id만으로는 부족하고, transfer session id, chunk window, receiver save stage가 필요하다. 회의 presence 로그와 전송 로그를 같은 타임라인에 섞어 보면 원인 분리는 가능하지만, 제품 문장이 하나면 사용자에게는 여전히 한 번의 실패로 남는다.
그 밤의 교훈은 성능 튜닝 체크리스트가 아니었다. 제품 경계 체크리스트였다. 어느 화면이 이 실패를 소유하는가. 어느 지원 문장이 이 실패를 설명하는가. 어느 로그 축이 이 실패를 재현하는가. 세 질문에 같은 이름 “PonsLink room”만 답으로 나오면, 이미 과부하 상태다.
제품 밖으로 뺀 것
PonsWarp로 빼낸 것은 코드 일부만이 아니었다.
- 전송 진행 상태를 화면 중앙에 둘 권리
- 브라우저별 저장 경로 차이를 숨기지 않을 권리
- 서버가 파일을 소유하지 않는다는 약속을 제품 문장으로 쓸 권리
- 회의 presence와 무관하게 전송 세션을 설명할 권리
- 전송 실패를 회의 실패와 다른 사과 문장으로 말할 권리
PonsLink 안에 작은 첨부 공유가 남을 수는 있다. 하지만 대용량 직접 전송의 실패 표를 room shell이 책임지면, 회의 제품의 얼굴이 전송 장애로 오염된다.
분리 후에도 기술 자산은 공유할 수 있다. signaling 경험, ICE 관측, 브라우저 제약 메모는 공통이다. 공유하면 안 되는 것은 “이 제품이 무엇을 끝이라고 부르는가”다. 회의의 끝은 대화 가능 상태이고, 전송의 끝은 저장 확인이다. 끝의 정의가 다르면 제품 이름도 달라야 한다.
밖으로 빼는 작업은 감정적 저항을 부른다. 한 제품 안에서 다 되는 모습이 더 멋져 보이기 때문이다. 하지만 멋져 보이는 통합이 운영 불가능한 통합이면, 사용자는 결국 더 단순한 외부 도구로 도망간다. 분리는 포기가 아니라 신뢰 회복 전략이다.
내부 계약이 바뀐 부분
분리 후 내부 계약을 다시 적었다.
- PonsLink는 세션 권한과 만남 운영을 보장한다
- PonsWarp는 전송 경로와 완료/실패 설명을 보장한다
- 공통 WebRTC 스택을 쓰더라도 제품 SLA 문장을 섞지 않는다
- “전송 중” 상세는 전송 제품의 정보 구조로 설계한다
- 회의 화면의 파일 진입점은 전송 제품으로의 명확한 핸드오프여야 한다
이 계약이 있어야 다음 구현에서 signaling 개선이 회의 UX를 깨지 않고, 전송 백프레셔 개선이 방 presence를 건드리지 않는다. 레포 구조보다 먼저 필요한 것은 이 문장들이다.
계약은 문서 한 장으로 끝나지 않는다. 이슈 템플릿, 릴리즈 노트, 지원 매크로, 로그 필드 이름까지 같은 경계를 따라야 한다. 코드만 분리하고 운영 언어가 그대로면, 한 달 뒤 다시 같은 화면에 기능이 돌아온다. 경계는 습관이 되어야 유지된다.
방에 남길 것과 뺄 것
모든 파일 기능을 밖으로 빼야 한다는 뜻은 아니다. 작은 첨부, 스크린샷, 짧은 메모 파일은 방 안 공유로 남을 수 있다.
기준은 크기보다 실패의 무게다.
- 실패해도 대화가 계속되면 방 안에 둘 수 있다
- 실패가 세션 전체의 신뢰 붕괴로 이어지면 밖으로 빼야 한다
- 브라우저별 저장 설명이 회의 문맥을 압도하면 밖으로 빼야 한다
- 진행률이 화면 중앙을 차지해야 하면 이미 전송 제품이다
이 기준으로 보면 PonsWarp 분리는 감정적 결별이 아니라, 회의 제품의 얼굴을 지키기 위한 최소 조치였다. 방 안에서 전송을 붙일 때 빠지기 쉬운 함정은 “진행률만 있으면 된다”는 착각이다. 진행률은 필요하지만 충분 조건이 아니다. 사용자는 진행률 숫자와 함께 다음을 알고 싶어 한다.
- 지금 연결 중인가 전송 중인가 저장 중인가
- 상대가 같은 상태를 보고 있는가
- 실패하면 어디부터 다시인가
- 브라우저를 닫으면 무엇이 사라지는가
이 네 질문이 회의 UI 밀도 안에서 답해지지 않으면, 전송은 이미 잘못된 집에 있는 것이다. 나는 그 집을 억지로 넓히기보다 전송 전용 집을 짓는 쪽을 택했다.
또 하나, 용량 제한 문구는 정책을 알려 줄 뿐 신뢰를 만들지는 않는다. “100MB까지”라고 써 두어도 80MB에서 저장 실패가 나면 사용자는 제한이 아니라 제품을 탓한다. 제한보다 완료 정의가 먼저다. 완료 전에 성공 톤의 UI를 보여 주면, 마지막 실패가 배신으로 읽힌다.
보안 이야기도 같은 맥락이다. 서버 미보관은 강한 문장이지만, 방 제품 문맥에서는 자주 묻힌다. 회의 화면의 주인공은 사람이고 전송 상세는 조연이다. 조연이 보안 약속을 연기하면 대사가 안 들린다. 그래서 그 대사를 제대로 할 수 있는 무대로 옮긴 것이다.
지금 돌이켜 보면, 파일 버튼 하나를 방에 넣느라 쓴 시간보다, 그 버튼이 실패했을 때 회의 제품이 잃을 신뢰를 계산하는 시간이 더 중요했다. 기능 비용과 신뢰 비용은 단위가 다르다. 이 글의 독자가 같은 실수를 피하려면, 방 안에 파일 버튼을 넣기 전에 실패 리허설을 해 보면 된다. 일부러 수신 탭을 새로고침하고, 저장 권한을 거부하고, 중간 크기의 파일로 백프레셔를 만든다. 그때 회의 화면이 얼마나 당황스러운 얼굴이 되는지 보면, 분리가 과한지 부족한지 감이 온다.
나는 그 리허설 후에야 “이건 부가 기능이 아니다”라고 말할 수 있었다. 직감이 아니라 실패 얼굴이 결정을 밀어 올렸다.
전송 기능을 방에 둘지 말지 고민 중인 팀에 말하고 싶은 핵심은 하나다. 코드 재사용 가능성과 제품 얼굴의 오염 가능성을 같은 저울에 올려라. 재사용이 조금 불편해져도 얼굴이 덜 더러워진다면, 그 불편은 대개 값싸다. 같은 결론을 다음 주 우선순위 회의에서도 다시 꺼내 보려 한다.
마치며
PonsWarp는 아이디어 스케치에서 태어나지 않았다. PonsLink 방에서 파일 전송이 먼저 고장 난 결과다.
기능이 비슷해 보여도, 실패했을 때 사과할 문장이 다르면 제품을 나누는 편이 낫다. 나는 그 판단을 기능 로드맵이 아니라 실패 밤의 로그에서 얻었다.
지금 다시 설계한다면 방 안에 파일 버튼을 넣기 전에, 그 버튼이 실패했을 때 회의 제품의 얼굴을 얼마나 더럽히는지부터 계산하겠다. 그 계산이 분리 여부보다 먼저다.