YouTube·Facebook 동시 송출, 6단계 수동 설정을 1번 인증으로 — OAuth 자동화 구현기
라이브 스트리밍 플랫폼에서 소셜 미디어 동시 송출을 위한 6단계 수동 설정 과정을 OAuth 2.0 기반 자동화로 1회 인증으로 줄인 구현 과정을 공유합니다.
참고: 이 글의 코드는 실제 업무에서 경험한 내용을 바탕으로 개념적으로 재작성한 것입니다. 실제 회사 코드와는 무관합니다.
OAuth 2.0 인증 + 자동 송출 흐름
들어가며
Catenoid의 라이브 스트리밍 솔루션을 사용하는 고객들은 YouTube나 Facebook에 동시 송출(Syndication)하기 위해 매번 이런 과정을 거쳐야 했습니다.
- YouTube Studio에 접속
- 로그인
- "스트리밍 시작" 클릭
- 스트림 키와 서버 주소 복사
- Catenoid 관리 콘솔로 돌아와서 붙여넣기
- 송출 시작
6단계. 그리고 이 과정에서 스트림 키를 잘못 복사하거나, 오타가 생기면 바로 CS로 이어졌습니다. "왜 동시 송출이 안 되나요?"라는 문의의 대부분이 스트림 키 입력 오류였습니다.
이 글은 이 문제를 OAuth 2.0 기반 자동화로 해결한 과정입니다.
설계: 백엔드에서 모든 것을 처리하기
핵심 접근법은 사용자 대신 백엔드가 소셜 플랫폼 API를 직접 호출하는 것이었습니다.
[사용자] → [계정 연동 (1회)] → [OAuth 토큰 저장]
[사용자] → [송출 시작 클릭]
↓
[백엔드 자동 처리]
1. 저장된 토큰으로 YouTube API 호출
2. 라이브 방송 생성
3. 스트림 키 획득
4. Catenoid 스트리밍 엔진 자동 설정
5. 송출 시작
사용자는 최초 1회만 "YouTube 계정 연결"을 하면, 이후 모든 과정이 자동으로 이루어집니다.
OAuth 2.0 플로우 구현
1. 플랫폼별 앱 등록
YouTube와 Facebook 각각의 개발자 콘솔에서 앱을 등록하고 필요한 권한(Scope)을 신청했습니다.
YouTube 필요 권한:
- https://www.googleapis.com/auth/youtube
- https://www.googleapis.com/auth/youtube.readonly
Facebook 필요 권한:
- publish_video
- pages_manage_posts
- pages_read_engagement
2. 인가 코드 플로우 구현
// 인증 URL 생성 (사용자를 YouTube 로그인 페이지로 리디렉션)
router.get('/auth/youtube/connect', (req, res) => {
const authUrl = `https://accounts.google.com/o/oauth2/auth?` +
`client_id=${YOUTUBE_CLIENT_ID}&` +
`redirect_uri=${encodeURIComponent(REDIRECT_URI)}&` +
`scope=${encodeURIComponent(YOUTUBE_SCOPES)}&` +
`response_type=code&` +
`access_type=offline&` // Refresh Token 발급을 위해 필수
`prompt=consent`; // 항상 Refresh Token 받기 위해
res.redirect(authUrl);
});
// Redirect URI 처리: 인가 코드를 토큰으로 교환
router.get('/auth/youtube/callback', async (req, res) => {
const { code } = req.query;
const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
body: new URLSearchParams({
code,
client_id: YOUTUBE_CLIENT_ID,
client_secret: YOUTUBE_CLIENT_SECRET,
redirect_uri: REDIRECT_URI,
grant_type: 'authorization_code'
})
});
const { access_token, refresh_token, expires_in } = await tokenResponse.json();
// 토큰 암호화 후 DB 저장
await saveTokens(req.user.id, 'youtube', {
accessToken: encrypt(access_token),
refreshToken: encrypt(refresh_token),
expiresAt: new Date(Date.now() + expires_in * 1000)
});
res.redirect('/settings/social?connected=youtube');
});
3. Access Token 자동 갱신
Access Token은 보통 1시간 후 만료됩니다. Refresh Token으로 자동 갱신하는 로직이 필수입니다.
async function getValidAccessToken(userId: string, platform: string): Promise<string> {
const tokens = await db.socialTokens.findOne({ userId, platform });
// 만료 5분 전부터 갱신
if (tokens.expiresAt < new Date(Date.now() + 5 * 60 * 1000)) {
const refreshed = await refreshAccessToken(
platform,
decrypt(tokens.refreshToken)
);
await db.socialTokens.update({
where: { userId, platform },
data: {
accessToken: encrypt(refreshed.access_token),
expiresAt: new Date(Date.now() + refreshed.expires_in * 1000)
}
});
return refreshed.access_token;
}
return decrypt(tokens.accessToken);
}
플랫폼 추상화: 새 플랫폼 추가를 쉽게
YouTube와 Facebook의 API는 완전히 다릅니다. 이를 공통 인터페이스로 추상화했습니다.
interface SocialStreamProvider {
createLiveBroadcast(title: string, scheduledAt: Date): Promise<string>;
getStreamKey(broadcastId: string): Promise<StreamKey>;
startBroadcast(broadcastId: string): Promise<void>;
endBroadcast(broadcastId: string): Promise<void>;
}
class YouTubeProvider implements SocialStreamProvider {
async createLiveBroadcast(title: string, scheduledAt: Date): Promise<string> {
const response = await youtube.liveBroadcasts.insert({
part: ['snippet', 'status', 'contentDetails'],
requestBody: {
snippet: { title, scheduledStartTime: scheduledAt.toISOString() },
status: { privacyStatus: 'public' },
contentDetails: { enableAutoStart: true }
}
});
return response.data.id;
}
// ...
}
class FacebookProvider implements SocialStreamProvider {
async createLiveBroadcast(title: string): Promise<string> {
const response = await fb.api(
`/${PAGE_ID}/live_videos`,
'POST',
{ title, status: 'LIVE_NOW' }
);
return response.id;
}
// ...
}
새로운 플랫폼(예: Twitch)을 추가할 때 이 인터페이스만 구현하면 됩니다.
자동화 워크플로우
사용자가 "송출 시작"을 클릭하면 백엔드에서 이 과정이 자동으로 실행됩니다:
async function startSyndication(channelId: string, platforms: string[]) {
const results = await Promise.allSettled(
platforms.map(async (platform) => {
const provider = getProvider(platform); // YouTube | Facebook
const accessToken = await getValidAccessToken(channelId, platform);
// 1. 라이브 방송 생성
const broadcastId = await provider.createLiveBroadcast(channel.title);
// 2. 스트림 키 획득
const streamKey = await provider.getStreamKey(broadcastId);
// 3. Catenoid 스트리밍 엔진에 설정
await cateonoindStreamingEngine.addDestination({
channelId,
rtmpUrl: streamKey.ingestionAddress,
streamKey: streamKey.key
});
// 4. 방송 시작
await provider.startBroadcast(broadcastId);
return { platform, broadcastId, status: 'STARTED' };
})
);
// 실패한 플랫폼 처리
results.forEach(result => {
if (result.status === 'rejected') {
log.error('Syndication failed:', result.reason);
// 알림 발송
}
});
}
Promise.allSettled를 사용해 한 플랫폼이 실패해도 다른 플랫폼의 송출은 계속되도록 했습니다.
결과
- 6단계 수동 설정 → 1회 계정 연동으로 완전 자동화
- 스트림 키 오입력으로 인한 CS 대폭 감소
- YouTube, Facebook 동시 송출 완전 자동화 달성
OAuth 구현 시 주의사항
실제 구현하면서 놓치기 쉬웠던 포인트들:
access_type=offline필수: Refresh Token을 받으려면 반드시 포함해야 함prompt=consent설정: 이미 인증한 계정이라도 다시 동의화면을 띄워 Refresh Token을 항상 받을 수 있게- 토큰 암호화 저장: DB에 평문으로 저장하면 안 됨. AES-256 등으로 암호화
- 만료 전 갱신: 정확한 만료 시점보다 여유 있게(5분 전) 갱신해야 Race Condition 방지