<pname>RAM 프리로딩으로 영상 재생 지연 제거하기</pname>
<tag>회고,개발</tag>
구현 배경
인상적인 연출을 위해 프로그램 내에 3D 영상을 도입하는 기획이 있었다. 시작 화면에서 키보드를 누르면 드래프트 시작을 알리는 영상이 재생되며 다음 씬으로 넘어가는 구조였다.
그런데 실제로 시작 화면에서 키보드를 누르자, 영상이 바로 재생되지 않고 짧은 시간 동안 검은 화면이 나타난 뒤에야 영상이 재생되는 문제가 발생했다. 이 짧은 공백이 시청자의 몰입감을 저해하는 요소라고 판단했다.
연출의 핵심은 "키를 누르는 순간 → 즉시 영상"이라는 매끄러운 흐름인데, 중간에 검은 화면이 끼면 그 리듬이 깨진다. 반드시 없애야 하는 문제였다.
원인 분석
두 가지 원인이 있었다.
첫째, 씬에서 사용하는 스프라이트 리소스가 필요한 시점에 디스크에서 로딩되면서 지연이 생겼다.
둘째, 비디오 재생 시 매 프레임 디코딩이 한꺼번에 일어나면 프레임 드롭이 발생했다.
이를 해결하기 위해 이 프로그램은 방송용이라는 특성을 이용하고자 했다.
방송용 프로그램은 실행(준비) 단계와 실제 방송(런타임) 단계가 명확히 분리되어, 방송 전에 프로그램을 미리 켜두는 것이 자연스러운 워크플로우였고 이 특성을 역이용하여 "실행 시 로딩은 오래 걸려도 되지만, 런타임에는 어떤 지연도 없어야 한다"는 방향으로 구성했다.

1. 스프라이트 프리로딩
로딩 씬에서 사용할 스프라이트를 배열에 등록해두고, Step마다 청크 단위로 sprite_prefetch를 호출해 RAM에 미리 올린다. 한 프레임에 전부 올리지 않고 1개씩 나눠 처리하기 때문에 로딩 중 프레임 드롭이 없다.
// obj_loading Create video_chunks = [ spr_background_draft_01, spr_background_draft_02, // ... ]; chunk_index = 0; frames_per_step = 1; loading_done = false; // obj_loading Step if (!loading_done) { for (var i = 0; i < frames_per_step && chunk_index < array_length(video_chunks); i++) { var spr = video_chunks[chunk_index]; if (sprite_exists(spr)) sprite_prefetch(spr); chunk_index++; } if (chunk_index >= array_length(video_chunks)) { loading_done = true; room_goto(room_staff); } }
2. Video 캐싱
video_open으로 비디오를 열어두고, Draw 이벤트에서는 매 프레임 직접 디코딩하는 대신 surface에 캐싱된 마지막 프레임을 그린다. 4프레임마다 한 번 decode를 예약(video_draw_pending)해 디코딩 부하를 분산했다.
// obj_videoplayer Draw if (video_get_status() == video_status_playing) { if (global.video_surface == -1 || !surface_exists(global.video_surface)) { global.video_surface = surface_create(global.video_width, global.video_height); } draw_surface_ext(global.video_surface, 0, 0, 1, 1, 0, c_white, 1); if (floor(global.video_frame_mod) mod 4 == 0) { global.video_draw_pending = true; } global.video_frame_mod += 1; } // obj_videoplayer Step if (global.video_draw_pending) { var data = video_draw(); if (data[0] == 0) { var s = data[1]; if (surface_exists(s) && surface_exists(global.video_surface)) { surface_set_target(global.video_surface); draw_surface(s, 0, 0); surface_reset_target(); } } }
3. ffmpeg로 로딩 부담 줄이기
프리로딩은 리소스를 통째로 메모리에 올리는 방식이라, 원본 용량이 크면 그만큼 로딩 시간과 메모리 사용량이 커진다. 그래서 ffmpeg를 사용해 영상 용량을 줄였다. 연출에 필요한 화질을 유지하는 선에서 코덱·비트레이트·해상도를 조정해 부담을 낮췄다.
ffmpeg -i input.mp4 -c:v libx264 -crf 23 -preset slow -vf "scale=1280:-2" output.mp4
crf: 화질/용량 트레이드오프 (값이 클수록 용량↓ 화질↓)
preset: 인코딩 시간 vs 압축 효율
vf scale: 필요 이상으로 큰 해상도를 낮춰 용량 절감
결과 및 회고
이 해결책의 본질은 "시작 로딩 시간 ↔ 런타임 지연"의 교환이다.
- 희생한 것: 프로그램 실행 시 로딩이 길어진다. 영상을 미리 메모리에 올리므로 메모리 사용량도 늘어난다.
- 얻은 것: 런타임에는 키 입력 → 재생 사이의 지연이 0에 수렴한다.
일반적인 게임/앱이라면 시작 로딩이 길어지는 건 오히려 나쁜 UX다. 하지만 방송용 프로그램은 방송 전에 미리 켜두는 것이 전제이므로, 시작 로딩 시간은 사실상 비용이 아니다. 즉, 프로그램의 사용 맥락을 정확히 파악했기 때문에 성립하는 트레이드오프였다. 맥락이 달랐다면 다른 선택(스트리밍 디코딩, 첫 프레임만 프리로딩 등)을 했을 것이다.
- 키보드를 누른 즉시, 검은 화면 없이 바로 영상이 재생되고 씬이 전환되도록 구현했다.
- 자연스러운 트랜지션 연출이 완성되었고, 시청자들에게 참신한 연출이라는 긍정적인 평가를 받았다.
- ffmpeg 최적화로 프리로딩에 드는 시간·메모리 부담도 함께 줄였다.
- 성능 문제는 항상 "빠르게 만들기"가 아니라, 지연을 사용자가 신경 쓰지 않는 시점으로 옮기기로도 풀 수 있다는 걸 배웠다. 프리로딩은 그 대표적인 패턴이다.
- 최적의 해법은 그 소프트웨어가 쓰이는 맥락에서 나온다. "방송용이라 실행 단계와 런타임이 분리된다"는 특성을 인지한 게 이 해결의 핵심이었다.
- (심화 방향) 영상이 많거나 매우 크다면, 전체 프리로딩 대신 첫 몇 초만 메모리에 올리고 나머지는 백그라운드 스트리밍하는 하이브리드 방식으로 메모리와 지연을 동시에 관리할 수 있다.
<pagelist>
