영상 스트리밍 인증 구조 설계

영상 스트리밍 인증 구조 설계

- ReactPlayer 기반 영상 매뉴얼 서비스 구축 사례-

1. 프로젝트 개요

본 사례는 기존 PDF 기반 사용자 매뉴얼 시스템을 영상 콘텐츠 기반 서비스로 전환하기 위해 수행된 프로젝트입니다. 기존 시스템에서는 사용자가 매뉴얼 버튼을 클릭하면 Frontend가 AWS S3에 직접 접근하여 PDF 파일을 다운로드하는 구조를 사용하고 있었습니다. 이 방식은 구현은 단순했지만, 사용자가 실제 업무 화면과 매뉴얼을 동시에 확인하기 어렵고 복잡한 절차를 직관적으로 전달하기 어렵다는 한계가 있었습니다.

고객사는 사용자가 업무 화면을 유지한 상태에서 매뉴얼 영상을 함께 확인할 수 있도록 PIP(Picture in Picture) 기능을 요구하였습니다. 동시에 보안 정책상 AWS S3 직접 접근을 차단해야 했고, URL 파라미터에 민감한 인증 정보를 포함할 수 없다는 제약도 존재했습니다. 따라서 본 프로젝트의 핵심은 단순 영상 재생 기능 구현이 아니라, 브라우저 기반 영상 재생 구조와 보안 요구사항을 함께 만족하는 인증 및 스트리밍 구조를 설계하는 것이었습니다.

2. 기술 선정 및 설계 방향

Frontend 영역에서는 React 기반 영상 플레이어 라이브러리인 ReactPlayer를 도입하였습니다. ReactPlayer는 HTML5 video 태그 기반으로 동작하며 React 환경과의 호환성이 높고, PIP 기능과 재생 상태 제어가 비교적 간단하다는 장점이 있었습니다.

다만 ReactPlayer는 내부적으로 video 태그의 요청 방식을 사용하므로 영상 파일 요청이 기본적으로 GET 방식으로 수행됩니다. 일반적인 API 호출처럼 요청 본문에 인증 정보를 담거나 자유롭게 헤더를 구성하기 어렵기 때문에, 인증 과정과 실제 영상 재생 요청을 분리하는 설계가 필요했습니다.

샘플 코드 1. ReactPlayer 호출 예시

ReactPlayer
url={videoEndpoint}
controls
pip
playing={false}
width="100%"
height="100%"
/>

위 예시는 ReactPlayer가 서버에서 전달받은 영상 재생 엔드포인트(endpoint)를 기반으로 영상을 재생하는 구조를 나타냅니다. 인증 정보는 URL에 포함하지 않고, 사전 인증 단계에서 발급된 임시 토큰을 Cookie로 전달하는 방식으로 처리하였습니다.

3. 아키텍처 재설계

기존 구조는 Frontend가 AWS S3에 직접 접근하는 방식이었습니다. 그러나 영상 콘텐츠 제공 시 동일한 구조를 유지하면 스토리지 경로 노출, URL 재사용, 접근 제어 우회 가능성이 발생할 수 있었습니다. 이에 따라 다음과 같이 서버 역할을 분리하였습니다.

Frontend
-> Server A (사용자 인증 및 스트리밍 요청 중계)
-> Server B (파일 조회 전용 서버)
-> AWS S3

Server A는 사용자 인증, 권한 검증, 임시 토큰 발급, 스트리밍 요청 중계를 담당합니다. Server B는 실제 영상 파일 조회와 AWS S3 접근만 담당하도록 분리하였습니다. 이를 통해 클라이언트가 스토리지 경로를 직접 알 수 없도록 차단하고, 모든 영상 요청이 서버의 인증 검증을 통과하도록 구성하였습니다.

4. 인증 및 재생 흐름

사용자가 매뉴얼 버튼을 클릭하면 영상 플레이어를 즉시 호출하지 않고, 먼저 Server A로 사용자 검증 API를 요청합니다. 검증이 완료되면 Server A는 영상 재생용 endpoint와 유효 기간이 제한된 임시 토큰을 발급합니다. 임시 토큰은 브라우저 Cookie에 저장되며, 이후 ReactPlayer가 영상 endpoint를 호출할 때 함께 전달됩니다.

샘플 코드 2. 영상 재생 정보 요청

const openManualVideo = async (manualId: string) => {
const response = await api.get(`/api/videos/${manualId}/authorize`);
setVideoEndpoint(response.data.videoEndpoint);
setManualModalOpen(true);
};

위 코드는 매뉴얼 버튼 클릭 시 영상 재생 endpoint를 먼저 요청하는 예시이다. 이 단계에서 서버는 사용자 권한을 검증하고 임시 토큰을 Cookie에 저장하도록 응답합니다.

샘플 코드 3. 임시 토큰 Cookie 발급 예시

ResponseCookie cookie = ResponseCookie.from("VIDEO_ACCESS_TOKEN", token)
.httpOnly(true)
.secure(true)
.path("/api/videos/")
.maxAge(Duration.ofMinutes(30))
.sameSite("Strict")
.build();

return ResponseEntity.ok()
.header(HttpHeaders.SET_COOKIE, cookie.toString())
.body(new VideoAuthorizeResponse(videoEndpoint));

Cookie에는 HttpOnly, Secure, SameSite 옵션을 적용하여 스크립트 접근과 불필요한 외부 전송을 제한하였습니다. 또한 path와 maxAge를 지정하여 영상 요청에만 제한적으로 사용되도록 설계하였습니다.

5. 스트리밍 중계 처리

영상 스트리밍은 일반 파일 다운로드와 달리 대용량 데이터를 지속적으로 전달하며, 사용자가 재생 위치를 이동할 때 Range Header 기반 부분 요청이 발생할 수 있습니다. 따라서 Server A는 클라이언트의 Range Header를 Server B로 전달하고, Server B의 응답 상태와 주요 헤더를 다시 클라이언트로 전달해야 합니다.

샘플 코드 4. Range Header 기반 스트리밍 중계

@GetMapping("/api/videos/{manualId}/stream")
public Mono >> stream(
@PathVariable String manualId,
@RequestHeader(value = HttpHeaders.RANGE, required = false) String range
) {
return webClient.get()
.uri("/internal/files/{manualId}", manualId)
.headers(headers -> {
if (range != null) headers.set(HttpHeaders.RANGE, range);
})
.exchangeToMono(response -> {
Flux body = response.bodyToFlux(DataBuffer.class);
ResponseEntity.BodyBuilder builder = ResponseEntity.status(response.statusCode());
response.headers().asHttpHeaders().forEach((name, values) -> {
if (isStreamingHeader(name)) builder.header(name, values.toArray(String[]::new));
});
return Mono.just(builder.body(body));
});
}

이 방식의 핵심은 bodyToMono(Resource.class) 또는 bodyToMono(byte[].class)처럼 전체 파일을 메모리에 적재하지 않는 것입니다. bodyToFlux(DataBuffer.class)를 사용하면 영상 데이터를 청크(chunk) 단위로 전달할 수 있어 대용량 영상 처리에 적합합니다.

샘플 코드 5. 주요 스트리밍 헤더 필터링

private boolean isStreamingHeader(String name) {
return HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)
|| HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)
|| HttpHeaders.CONTENT_RANGE.equalsIgnoreCase(name)
|| HttpHeaders.ACCEPT_RANGES.equalsIgnoreCase(name)
|| HttpHeaders.CACHE_CONTROL.equalsIgnoreCase(name);
}

Content-Range, Accept-Ranges, Content-Length와 같은 헤더는 브라우저가 영상의 길이와 부분 응답 범위를 판단하는 데 필요합니다. 특히 탐색기능과 안정적인 재생을 위해 이러한 헤더를 보존하는 것이 중요합니다.

6. 결과 및 효과

구현 결과 사용자는 브라우저에서 즉시 영상을 재생할 수 있었고, PIP 기능을 통해 실제 업무 화면과 영상 매뉴얼을 동시에 활용할 수 있게 되었습니다. 또한 AWS S3 직접 접근을 제거하고 서버 기반 접근 제어를 적용함으로써 보안 요구사항을 충족할 수 있었습니다.

• AWS S3 직접 노출 제거

• URL 기반 민감 정보 전달 제거

• 사용자 검증 후 영상 endpoint 접근 허용

• 임시 토큰 기반 제한적 접근 제어

• Range Header 기반 영상 재생 및 탐색 지원

7. 결론

본 사례를 통해 단순히 영상 플레이어를 추가하는 작업이 아니라, ReactPlayer와 HTML5 video 태그의 동작 방식, 고객사의 보안 정책, 서버 간 역할 분리, 스트리밍 처리 방식을 종합적으로 고려한 설계 사례를 경험하게 되었습니다. 인증과 재생 과정을 분리하고, Server A와 Server B의 책임을 명확히 나눔으로써 사용자 경험과 보안성을 동시에 확보할 수 있었습니다.

특히 실무 환경에서는 기능 구현 자체보다 보안 정책과 브라우저 동작 방식의 제약을 함께 이해하는 것이 중요하다고 느꼈습니다. 이번 프로젝트를 통해 영상 기반 매뉴얼 서비스에서도 인증 구조, 데이터 흐름, 스트리밍 헤더 처리, 운영 보안 정책을 함께 고려해야 안정적인 서비스 제공이 가능하다는 점을 확인할 수 있었습니다.

NZ

Site footer