Unity
"제 결과물은 어디서 봐요?" — QR 한 번으로 7개 부스 기록을 불러오는 AI 아카이브 개발기
김*균··수정됨 2026.10.22
QR 한 번으로 7개 부스의 기록을 모아 보는 — AI 아카이브 키오스크
전시장을 한 바퀴 돌며 AI 탐정, AI 스카이 메이커, AI 이스케이프 룸, AI 보이스 시프트, AI 작가, AI 스튜디오, AI 영화 감독까지 일곱 개의 체험을 마친 관람객은 마지막으로 아카이브 부스에 도착합니다. 키오스크 앞에서 자신의 QR을 스캔하면, 화면에는 지금까지 쌓은 점수로 매겨진 전체 랭킹과 각 부스에서 AI와 함께 만든 결과물(포스터·영상·음악)이 한꺼번에 펼쳐집니다. 이번 포스팅에서는 이 아카이브 키오스크의 세션 처리 구조, 늦게 도착한 응답을 걸러내는 방법, 그리고 결과물 영상을 끊김 없이 재생하기까지의 구현 과정을 공유합니다.
1. "시스템 아키텍처 개요"
키오스크는 Unity 단일 씬으로 구성된 Windows 스탠드얼론 앱입니다. 서버와는 두 갈래로 통신합니다. WebSocket으로는 "지금 이 부스에서 누가 QR을 찍었다"는 세션 이벤트를 실시간으로 받고, REST API로는 그 세션의 리더보드와 결과물 목록을 조회합니다. 화면 전환은 ArchiveSessionManager의 상태 이벤트를 ArchiveScreenFlow가 구독해, 해당 화면 GameObject만 SetActive로 켜고 끄는 방식입니다.
세션은 Disconnected → Connecting → WaitingForSession → Starting → Loading → Ready → Submitting 순으로 진행되며, 체험 도중 일정 시간 입력이 없거나 서버가 세션을 중단하면 Aborted를 거쳐 다시 대기 상태로 돌아옵니다. UI는 이 상태만 바라보면 되도록 네트워크 계층과 화면 계층을 분리했습니다.
주요 기술적 구현
2.1 메인스레드로 모으는 WebSocket 이벤트
WebSocket은 System.Net.WebSockets.ClientWebSocket을 사용해 별도 플러그인 없이 구현했습니다. 문제는 수신 루프가 백그라운드 Task에서 돈다는 점입니다. Unity API는 메인스레드에서만 호출할 수 있기 때문에, 수신한 메시지를 바로 처리하지 않고 ConcurrentQueue에 "할 일"로 쌓아 둔 뒤 Update()에서 매 프레임 꺼내 실행합니다.
이렇게 하면 OnStartSession, OnAck 같은 모든 이벤트가 메인스레드에서 발생하므로, 이벤트를 구독하는 UI 코드는 스레드를 전혀 신경 쓰지 않아도 됩니다. 연결이 끊기면 1, 2, 4 … 최대 30초 간격의 지수 백오프로 재연결하고, 서버가 같은 부스의 다른 연결로 교체했다는 의미의 close code(4001)를 받았을 때만 재연결을 멈춥니다.
2.2 한 번에 한 세션만 — 상태 기반 세션 수락
전시장에서는 앞사람이 화면을 보고 있는 사이 뒷사람이 QR을 찍는 일이 흔합니다. 그래서 START_SESSION은 키오스크가 WaitingForSession 상태일 때만 받아들이고, 이미 종료 처리한 세션 id는 HashSet에 기록해 같은 메시지가 다시 와도 무시하도록(멱등) 했습니다.
세션을 수락하면 SESSION_STARTED를 보내고 서버의 ACK를 기다립니다. 정해진 시간 안에 ACK가 오지 않으면 같은 메시지를 다시 보내, 네트워크가 잠깐 흔들려도 세션이 시작 단계에서 멈춰 버리지 않게 했습니다.
2.3 늦게 도착한 응답 걸러내기
ACK를 받으면 리더보드와 결과물 두 API를 동시에 호출합니다. 여기서 까다로운 경우가 생깁니다. 로딩 중에 관람객이 홈 버튼을 눌러 취소했는데 이전 요청의 응답이 뒤늦게 도착하면, 메인 화면에 있던 키오스크가 갑자기 랭킹 화면으로 튀어 버립니다. 다음 관람객의 세션이 이미 시작된 뒤라면 옛 응답이 새 데이터를 덮어쓰기도 합니다.
이를 막기 위해 요청을 보낼 때의 세션 id를 loadId로 붙잡아 두고, 응답이 오면 현재 세션 id와 비교합니다. 취소하면 _sessionId를 0으로 비우기 때문에, 그 뒤에 도착한 응답은 모두 Stale()로 판정되어 화면을 건드리지 못합니다. 두 응답이 모두 유효하게 도착했을 때만 Ready로 넘어갑니다.
2.4 나의 위치가 보이는 랭킹
랭킹 패널은 상단 포디움(1~3위), 내 순위 바, 순위 리스트로 구성됩니다. 서버는 상위 랭커 목록(topRankers)과 내 주변 순위(mySection)를 따로 주는데, 내가 TOP5 안에 있으면 mySection이 비어서 옵니다. 이 차이를 이용해 리스트를 두 가지 방식으로 구성합니다.
TOP5 안(4위)일 때와 TOP5 밖(10위)일 때의 랭킹 패널
4위 관람객은 1~6위 리스트에서 자기 행이 강조되고, 10위 관람객은 자기 앞뒤 순위와 함께 표시됩니다. 두 목록을 합칠 때 같은 사람이 두 번 나오지 않도록 VisitorId로 중복을 제거합니다. 배지는 총점 구간에 따라 예비 러너부터 마스터 러너까지 7단계로 정해집니다.
2.5 AI 생성 영상을 끊김 없이 재생하기
가장 시간이 많이 든 부분은 영상 재생이었습니다. https URL을 그대로 스트리밍하면 Windows 재생 백엔드(WMF)에서 끊김이나 프레임 정지가 잦았고, 일부 AI 생성 mp4는 B-프레임 타임스탬프, 자막 트랙, 색상 메타데이터 문제로 아예 검은 화면만 나왔습니다.
해결책은 영상을 로컬 캐시 파일로 먼저 내려받고, ffmpeg로 baseline 프로파일에 맞춰 재인코딩하는 것이었습니다. 자막 트랙은 제거하고 색 공간은 bt709로 통일합니다. 캐시 파일 이름은 URL의 SHA1 해시로 정해서 썸네일 캡처와 결과 모달이 같은 파일을 공유하고, 같은 URL을 동시에 요청하면 다운로드는 한 번만 일어나고 나머지는 콜백만 대기합니다. 세션이 준비되는 즉시 모든 결과물을 미리 로딩해 두기 때문에 관람객이 썸네일을 누르면 모달이 열리자마자 재생이 시작됩니다.
3. "마치며"
아카이브 부스는 전시의 마지막 장면입니다. 여러 부스에서 AI와 함께 만든 결과물이 흩어지지 않고 한 화면에 모일 때, 관람객은 자신의 체험을 하나의 이야기로 돌아보게 됩니다. 그 경험을 지키기 위해 화면 뒤에서는 중복 세션 차단, 늦은 응답 폐기, 영상 재인코딩 같은 작은 안전장치를 쌓았습니다. 관람객이 QR을 찍는 순간부터 자신의 순위와 결과물을 확인하기까지 아무것도 멈추거나 튀지 않는 것, 그게 이 콘텐츠의 목표였습니다.
읽기 도구
약 7분 읽기
이 글이 도움이 되었나요?
Work with GIWorks
프로젝트에 GIWorks의 경험이 필요하신가요?
전시·콘텐츠·엔지니어링 프로젝트의 아이디어와 고민을 들려주세요. 필요한 기술과 실행 방법을 함께 찾겠습니다.
문의하기