0%

Unity

게임 엔진으로 키오스크를 만들었다고? — Unity로 상용 포토부스를 출시하기까지 삽질의 기록

김*겸··수정됨 2026.07.22

게임 엔진으로 키오스크를 만들었다고? — Unity로 상용 포토부스를 출시하기까지 삽질의 기록

"Unity로 키오스크요? 미쳤..."

포토 키오스크. 네컷사진, 인생네컷 — 다들 한 번쯤은 찍어봤을 것이다. 그런데 그 안에 돌아가는 소프트웨어를 Unity 게임 엔진으로 만들었다고 하면?

"차라리 WPF나 Electron 쓰지 왜 Unity를?"이라는 반응이 정상이다. 하지만 이 프로젝트는 실시간 웹캠 프리뷰, ML 기반 배경 제거, 드래그 앤 드롭 스티커 편집, 애니메이션 UI 전환을 전부 하나의 앱에서 돌려야 했다. 그 순간 Unity가 "미친 선택"에서 "유일한 선택"이 됐다.


프로젝트 구조: 12개 화면, 하나의 씬

Ready → Mode → Chroma → Select → Quantity → Payment → WaitingForPayment 
→ Guide → Filming → CutWindow → Sticker → Printing

키오스크의 전체 플로우는 12단계 상태 머신으로 돌아간다. 그런데 Unity 씬은 딱 하나. 씬 전환 없이 GameManager 싱글톤이 상태를 바꾸면, FadeAnimationCtrl이 23개의 페이드 상태를 라우팅하면서 패널을 전환한다.

왜 단일 씬인가? DontDestroyOnLoad 지옥을 피하기 위해서다. 리셋이 필요하면 그냥 SceneManager.LoadScene(현재씬)으로 씬을 통째로 다시 로드한다. 거대한 리셋 함수 대신, Unity의 씬 로딩 자체가 리셋 메커니즘이 된 셈이다.


함정 1: "1500줄짜리 인쇄 컨트롤러" — 사진 한 장 뽑는 게 이렇게 어렵다고?

PrintController.cs — 이 파일 하나가 1,500줄이다. "사진 찍어서 프린터로 보내면 끝 아닌가?"라고 생각했다면, 현실은 이렇다:

캡처 전략: 3단계 폴백

순서방식설명1순위HiRes ScreenCapture (8x)1920×1080 화면을 15,360×8,640으로 캡처 후 크롭2순위RenderTexture 직접 합성Linear 컬러 스페이스에서 색이 날아가는 문제로 기본 비활성화3순위ReadPixels 폴백최후의 수단

"임시 스케일업" 트릭

캡처 직전에 대상 UI를 루트 캔버스로 이동시키고 2배로 확대한 뒤 스크린샷을 찍고, 즉시 원래 자리로 되돌린다. 화면 레이아웃은 안 바뀌는데 픽셀 밀도는 2배가 되는 마법. 화면 밖으로 삐져나가는 걸 방지하는 가드까지 구현했다.

이미지 처리 파이프라인

캡처 후에도 끝이 아니다:

미러링 → 회전 보정 → 방향 자동 감지 → 리샘플(3200×4800) → 센터 크롭 
→ CPU 샤프닝(Unsharp Mask) → 프린터용 변환 → QR용 리사이즈

DS-RX1 프린터는 세로 방향만 받는다. 가로 사진이면? 90도 돌려서 보내야 한다. 여기서 MirrorX, MirrorY, Rotate90, Rotate180 조합이 모드(세로/가로)에 따라 다르게 적용된다. 이거 하나 잘못되면 사진이 거울상으로 나오거나 뒤집혀서 출력된다. 전부 CPU 픽셀 연산(GetPixels32/SetPixels32)으로 처리한다.


함정 2: "프린터 다이얼로그가 뜨는데 어떻게 닫지?" — Win32 API 해킹

프린터에 인쇄 명령을 보내면 Windows가 "사진 인쇄" 다이얼로그를 띄운다. 키오스크에서 이건 치명적이다 — 고객이 마우스로 "확인"을 눌러야 한다고?

해결책: Win32 API P/Invoke로 다이얼로그를 자동으로 닫는다.

[DllImport("user32.dll")]
private static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
[DllImport("user32.dll")]
private static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll")]
private static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, uint dwExtraInfo);

FindWindow로 다이얼로그 창을 찾고 → SetForegroundWindow로 포커스를 가져온 뒤 → keybd_eventEnter 키를 프로그래밍적으로 눌러버린다. 게임 엔진에서 Windows 키보드 이벤트를 시뮬레이션하는 날이 올 줄은 몰랐을 것이다.

최종적으로는 전용 PhotoPrinterBridge.exe를 외부 프로세스로 실행하는 방식으로 진화했다. Unity 코루틴이 매 프레임 Process.HasExited를 폴링하면서 타임아웃 시 Process.Kill()로 안전하게 종료한다.


함정 3: "Linear 컬러 스페이스에서 사진이 하얗게 빠진다"

Unity를 Linear 컬러 스페이스로 설정하면 렌더링 품질이 올라가지만, RenderTexture로 캡처한 이미지가 감마 보정 없이 저장되면서 사진이 씻겨나간 것처럼 하얗게 나온다.

// linear = false를 명시적으로 설정해서 sRGB로 강제
new Texture2D(2, 2, TextureFormat.RGBA32, false, /* linear: */ false)

false 하나가 정상적인 출력물과 폐지의 차이다. RenderTexture 방식은 아예 비활성화하고 HiRes ScreenCapture 경로를 기본으로 전환했다.


함정 4: "AWS SDK 없이 클라우드에 올려야 한다" — SigV4 서명 직접 구현

촬영한 사진을 QR 코드로 다운로드받을 수 있게 하려면 클라우드에 업로드해야 한다. 네이버 클라우드(NCP) Object Storage를 사용하는데, Unity에는 AWS SDK가 없다.

AWS Signature V4 서명 체인을 처음부터 직접 구현했다:

HMAC-SHA256(kDate) → HMAC-SHA256(kRegion) → HMAC-SHA256(kService) → HMAC-SHA256(kSigning)
→ Canonical Request → String to Sign → Authorization Header

Credential은 StreamingAssets/ncp_secrets.json에서 런타임에 로드한다. SDK 없이 HTTP 서명 프로토콜을 날것으로 구현하는 건... 다시는 하고 싶지 않은 경험일 것이다.


함정 5: "ML 배경 제거가 GPU마다 다르게 동작한다"

VirtualBackgroundController는 Unity Barracuda + MediaPipe Selfie Segmentation 모델로 실시간 인물 세그멘테이션을 수행한다.

문제는 — GPU/드라이버에 따라 마스크가 반전되는 경우가 있다는 것. NVIDIA에서는 인물이 흰색, AMD에서는 인물이 검정색. 같은 모델, 같은 코드인데!

해결책: 자동 반전 감지 시스템.

첫 30프레임 동안 10×10 그리드(100개 포인트)를 샘플링
→ 평균값 > 0.6이면 마스크 반전 적용

추론 백엔드도 ComputePrecompiled 실패 시 CSharpBurst로 자동 폴백한다. GPU 컴퓨트 셰이더가 없는 장비에서도 (느리지만) 동작한다.


함정 6: "웹캠이 죽어도 키오스크는 살아야 한다"

Logitech C922 웹캠을 사용하는데, 장시간 운영하면 웹캠이 먹통이 되는 경우가 있다. USB 연결이 불안정하거나, 드라이버가 응답을 멈추거나.

대응 체계:

  • 4K 자동 협상: 3840×2160으로 요청 → 0.5초 후 실제 해상도 확인 → 4K 실패 시 1920×1080으로 재시작

  • 5초 간격 헬스 체크: isPlaying == false 또는 width <= 16(Unity의 미초기화 센티널) 감지 시 자동 재시작

  • 사전 초기화: Start()에서 미리 켜두고 CanvasGroup.alpha로 보이기/숨기기 — 촬영 시작할 때 수 초간 기다리는 문제 제거

  • UV 미러링: 전면 카메라 좌우반전을 uvRect.width = -1f로 GPU에서 처리 — 픽셀 복사 0


함정 7: "결제 승인은 됐는데 서버가 죽었다" — 결제 데이터 유실 방지

VAN 단말기(tPayDaemon, 127.0.0.1:6444)와 HTTP 통신으로 카드 결제를 처리한다. 문제는 카드 승인은 성공했는데 백엔드 서버 전송이 실패하는 경우.

카드 승인 → 즉시 로컬 JSON 백업 저장 → 백엔드 전송 시도 
→ 실패 시 재시도(설정 가능 횟수/간격) → 앱 재시작 시 미전송 건 일괄 재전송

승인 즉시 로컬에 백업하기 때문에, 네트워크가 끊겨도 결제 데이터는 절대 유실되지 않는다. 고객은 즉시 다음 단계로 진행할 수 있다.


현장 운영을 위한 숨겨진 장치들

키오스크는 개발자 없이 현장 스태프가 운영해야 한다. 그래서 곳곳에 비밀 인터랙션이 숨어있다:

조작기능숨겨진 버튼 10번 탭앱 최소화 (Win32 ShowWindow P/Invoke)결제 영역 10번 탭 (1회)관리자 모드 ON — 결제 10원으로 변경결제 영역 10번 탭 (2회)관리자 모드 OFF — 정상 금액 복구용지 카운터 버튼 10번 탭용지 잔량 690매로 리셋씬 리로드전체 키오스크 초기화

네트워크 핑 체커, 디스크 용량 체크, 콘솔 로그 내보내기(바탕화면에 .txt로 저장)까지 — 원격 접속 없이도 현장에서 문제를 진단할 수 있는 도구들이 내장되어 있다.


스티커 시스템: "게임 엔진이라 가능한 것"

드래그 앤 드롭 스티커 편집이 Unity라서 자연스럽게 구현됐다:

  • 팔레트의 스티커를 드래그하면 원본은 그대로, 클론이 생성돼서 따라온다

  • PointerEventData.pointerDrag를 클론으로 리다이렉트하는 트릭

  • 드롭 존, 삭제 존, 실시간 크기 조절 슬라이더

  • 인쇄 시: 스티커 위치/크기를 프레임 비율에 맞게 재매핑하여 인쇄용 프레임에 복제


결론: "안 되는 걸 되게 하는 기술"

이 프로젝트에서 마주한 진짜 적은 "Unity는 키오스크용이 아니다"라는 전제 그 자체였다.

  • 게임 엔진에서 Win32 API를 호출해서 프린터 다이얼로그를 자동으로 닫고

  • AWS SDK 없이 클라우드 서명 프로토콜을 날것으로 구현하고

  • ML 추론 결과가 GPU마다 달라지는 버그를 자동 감지 시스템으로 우회하고

  • Linear 컬러 스페이스의 함정을 false 하나로 극복하고

  • 1,500줄짜리 이미지 파이프라인으로 캡처부터 인쇄까지 모든 변환을 처리했다

게임 엔진은 게임만 만드는 도구가 아니다. 현실 세계의 하드웨어와 싸우는 무기가 될 수도 있다.

다만, 다시 하라면... 좀 고민될 것 같다. 🙃


읽기 도구

10분 읽기

이 글이 도움이 되었나요?

다음으로 읽기