처음엔 진짜 아무 생각 없었다.
팀원 한 명이 “Vimeo에 영상 올리면 되지 않을까요?” 라길래,
“ㅇㅋㅇㅋ~” 하고 작업 시작했는데…
아니 이게 뭐냐
- 프리티어 용량 찔끔
- 고화질 제한
- 업로드 제한까지?
프론트에서 영상 업로드까지 시키고 싶었는데, 이 구조로는 도저히 못 버틸 것 같았다.
S3로 하자니 너무 손이 많이 감
잠깐 AWS S3도 생각했는데,
그건 단순 저장소일 뿐이고…
- 영상 인코딩은?
- 썸네일 생성은?
- 스트리밍은?
이걸 전부 직접 하려면
Lambda + ffmpeg + CloudFront + 사후 처리 = 지옥
그래서 결국 다시 찾아봤다. 그게 바로…
왜 Mux로 갔는가?
- 업로드, 인코딩, 썸네일, 스트리밍까지 그냥 다 해줌
- Vimeo보다 개발자 문서가 훨씬 친절함
- 가격도 사용량 기반이라 유연하게 컨트롤 가능
- 프론트에서 직접 업로드 가능한 Direct Upload API도 지원함
항목 | Vimeo | AWS S3 | Mux |
가격 | 월정액 기반 (고화질은 고가) | 저장 용량/트래픽별 과금 | 사용량 기반 과금 (비교적 합리적) |
업로드 제한 | 주간/월간 용량 제한 있음 | 제한 없음 | 제한 없음 |
인코딩 | 자동 지원 | 직접 구현해야 함 | 자동 인코딩 제공 |
스트리밍 | 자동 제공 (임베딩 위주) | 직접 구성 (CloudFront 등 필요) | HLS 스트리밍 기본 지원 |
썸네일 | 기본 제공 | 직접 추출 필요 | 자동 생성 |
유저 영상 업로드 | 제약 많음 | API 구성 가능하지만 복잡 | Direct Upload API 제공 (프론트에서도 가능) |
Mux 업로드 구조
- 백엔드에서 업로드용 URL과 uploadId를 발급받고
- 프론트에서 Mux로 직접 업로드한 다음에
- 업로드한 영상 정보를 백엔드에 등록
정리하면 이렇게 되는데, 여기까지만 하면 큰일난다. 왜냐면…
mux에는 올라가는데, 백엔드에 'Webhook' 처리가 없으면 이후 처리가 안 됨!
영상이 인코딩 완료됐는지 확인도 안 되고, 썸네일도 못 불러오고, playbackId도 없음.
대충 만든 프론트 코드
일단 디자인이 안나왓기에 프론트먼저 할수는 있지만 정리만 하면 되니 만들었다 ㅎㅎ
const handleUpload = async() => {
if (!file) return setMessage('파일 선택해');
try {
// STEP 1. 업로드용 URL 발급
const res = await fetch(`${process.env.NEXT_PUBLIC_APP_URL_TEST}/video/uploadUrl`, {
method: 'POST',
headers: {
Authorization: 'Bearer YOUR_TEST_JWT',
},
});
const { uploadUrl, uploadId } = await res.json();
if (!uploadUrl || !uploadId) throw new Error('업로드 URL 발급 실패');
// STEP 2. Mux로 직접 업로드
const uploadRes = await fetch(uploadUrl, {
method: 'PUT',
headers: { 'Content-Type': file.type },
body: file,
});
if (!uploadRes.ok) throw new Error('Mux 업로드 실패');
// STEP 3. 백엔드에 업로드 정보 등록
const register = await fetch(`${process.env.NEXT_PUBLIC_APP_URL_TEST}/video/register`, {
method: 'POST',
headers: {
Authorization: 'Bearer YOUR_TEST_JWT',
'Content-Type': 'application/json',
},
body: JSON.stringify({
uploadId,
title: '테스트 영상 제목',
description: '설명 예시',
}),
});
if (!register.ok) throw new Error('영상 등록 실패');
const result = await register.json();
console.log(result);
} catch (error) {
if (error instanceof Error) setMessage(error.message);
}
};
근데 이거만 하면 ㅈ됨...
Mux에 업로드는 되지만, 인코딩이 끝났는지, 썸네일이 생성됐는지, 영상 스트리밍 URL은 뭔지 모름.
그래서 Mux Webhook 설정은 필수다.
Webhook으로 인코딩 완료 이벤트를 받으면, 그때 playbackId, 썸네일 URL, 상태 등을 DB에 저장하면 됨.
다만 로컬에서는 웹훅이 작동을 안한다 왜? 실서비스가 아니니까 반환되는 값이 없으니까 너 누기야를 왜치는 남자가 웹훅이니까.
@ApiOperation({ summary: '웹훅' })
@Post('webhook/mux')
@HttpCode(200)
async muxWebHook(@Req() req: Request, @Res() res: Response) {
return this.videoService.muxWebHook(req, res);
}
// 웹훅
async muxWebHook(req, res) {
const secret = process.env.NODE_ENV === 'production' ? process.env.MUX_WEBHOOK_SECRET_NEST : process.env.MUX_WEBHOOK_SECRET;
const signature = req.headers['mux-signature'] as string;
const rawBody = (req as any).rawBody;
// 서명 검증 로직
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
const isValid = signature?.includes(expected);
if (!isValid) {
Sentry.captureMessage('MUX Webhook Signature Invalid', {
level: 'warning',
extra: {
receivedSignature: signature,
expectedHash: expected,
eventHeaders: req.headers,
},
});
return res.status(403).send('Invalid signature');
}
const payload = JSON.parse(rawBody.toString());
if (payload.type === 'video.asset.ready') {
const asset = payload.data;
const playbackId = asset.playback_ids?.[0]?.id;
const uploadId = asset.upload_id;
const thumbnailUrl = `https://image.mux.com/${playbackId}/thumbnail.jpg`;
const playbackUrl = `https://stream.mux.com/${playbackId}.m3u8`;
const video = await this.videoRepository.findOne({
where: { uploadId },
});
if (video) {
video.playbackId = playbackId;
video.thumbnailUrl = thumbnailUrl;
video.videoUrl = playbackUrl;
await this.videoRepository.save(video);
}
}
return { received: true };
}
근데 로컬에서는 webhook 테스트가 안 됨
당연하다. Mux가 로컬 서버한테 콜백을 날릴 수가 없잖아 ㅠㅠ.
그래서 ngrok 써야 했다.
npx ngrok http 3000
그리고 Mux 대시보드에서 Webhook URL을 ngrok 주소로 등록하면 됨.
이거 하나 구현하느라 생각보다 시간이 꽤 들었다.
“Mux 써야지~” 하고 가볍게 시작했지만,
- 영상 업로드 흐름 정리
- webhook 보안 처리
- 썸네일/스트리밍 링크 저장
이런 것까지 하다 보면 생각보다 뎁스가 깊다.
그래도… 지금은 꽤 마음에 든다. 😌
'nest' 카테고리의 다른 글
vimeo 연동하기 (0) | 2025.05.20 |
---|---|
JWT로 마이그레이션 해봅시다. (0) | 2025.05.18 |
백그라운드 알람.... (0) | 2025.03.06 |
s3 스토리지 (0) | 2025.02.27 |
ec2배포 by 챗지피티 (0) | 2025.02.27 |