React + Media Streams API๋ฅผ ํ†ตํ•œ Web Recorder ๊ธฐ๋Šฅ ๊ตฌํ˜„

React + Media Streams API๋ฅผ ํ†ตํ•œ Web Recorder ๊ธฐ๋Šฅ ๊ตฌํ˜„

Profile Picture
adultlee
2023-11-14

์›๋ณธ

์„œ๋ฌธ

ํ•ด๋‹น ๋””์ž์ธ์—์„œ๋„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋“ฏ, ์šฐ๋ฆฌ ์„œ๋น„์Šค์—์„œ ํ™”๋ฉด์†ก์ถœ์„ ๋น„๋กฏํ•ด์„œ ๋…นํ™”๊ธฐ๋Šฅ์€ ๊ฐ€์žฅ ํ•„์ˆ˜์ ์ด๋ผ๊ณ  ํ•  ์ˆ˜ ์žˆ๋‹ค. ์—ฌ๊ธฐ์„œ ์‹คํŒจํ•œ๋‹ค๋ฉด, ์‚ฌ์‹ค ๊ทธ ์–ด๋–ค ๊ธฐ๋Šฅ๋„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ๊ทธ ๋•Œ๋ฌธ์— ์ข€ ๋” ์‹ฌํ˜ˆ์„ ๊ธฐ์šธ์—ฌ ์ž‘์—…์„ ์ง„ํ–‰ํ–ˆ์—ˆ๋‹ค.

๋…นํ™” ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•ด์„œ ์‰ฌ์šด ๋ฐฉ๋ฒ•์€ ํ•ด๋‹น ๊ธฐ๋Šฅ์„ ์ง€์›ํ•˜๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ฐพ์•„์„œ ์‚ฌ์šฉํ•˜๋Š”๊ฒƒ์ด์—ˆ๋‹ค.

ํ•˜์ง€๋งŒ ๋‹น์—ฐํ•˜๊ฒŒ๋„, ์šฐ๋ฆฌ๋Š” ์•„์ง ๋ฐฐ์šฐ๋Š” ์ž…์žฅ์ด๊ณ , ํ•ด๋‹น ํŽ˜์ด์ง€์˜ ๊ธฐ๋Šฅ์€ ํ•˜๋‚˜์˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์— ์ข…์†๋˜๊ธฐ ๋ณด๋‹ค๋Š” ์šฐ๋ฆฌ๊ฐ€ ๋Š์ž„์—†์ด ์ˆ˜์ •ํ•˜๊ณ  ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ์œ ์ง€๋ณด์ˆ˜๊ฐ€ ์šฉ์ดํ•ด์•ผํ•œ๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ๋‹ค. ๊ทธ์— ๋”ฐ๋ผ ์‹œ๊ฐ„์ด ๋‹ค์†Œ ๊ฑธ๋ฆฌ๋”๋ผ๋„ ์šฐ๋ฆฌ๊ฐ€ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก, ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—†์ด Web API์˜ ์ง€์›๋งŒ์œผ๋กœ ํ•ด๊ฒฐํ•˜๊ณ ์ž ํ–ˆ๋‹ค.

์•„๋ž˜๋ถ€ํ„ฐ๋Š” ๋‚ด๊ฐ€ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด์„œ ์ง„ํ–‰ํ•œ ์ƒ๊ฐ์˜ ํ๋ฆ„์„ ๋‹ด๊ณ  ์žˆ๋‹ค.

๋‚ด๊ฐ€ ํ•ด๊ฒฐํ•œ ๋ฐฉ๋ฒ•!

  1. ๊ฑฐ์šธ์˜ ๊ธฐ๋Šฅ์„ ์ˆ˜ํ–‰ํ•  MediaStream๊ณผ ๋…นํ™”๋ฅผ ์ง„ํ–‰ํ•  MediaRecord ๋ฅผ ๋‘๊ฐ€์ง€๋ฅผ ๋ณ‘๋ ฌ์ ์œผ๋กœ ์‚ฌ์šฉ
  2. ๋‚ด๋ถ€์˜ stream์„ ๊ด€๋ฆฌํ•  stream state์™€, record์—ฌ๋ถ€๋ฅผ ํŒ๋‹จํ•˜๋Š” booleanํƒ€์ž…์˜ state ๋‘ ๊ฐ€์ง€๋ฅผ ์‚ฌ์šฉ

1. ํ•„์š” ๊ธฐ๋Šฅ์ด ๋ฌด์—‡์ธ๊ฐ€.

์šฐ์„  ๊ฐœ๋ฐœ์„ ์ง„ํ–‰ํ•˜๊ธฐ ์ „, ํ•ด๋‹น ํ…Œ์Šคํฌ๋ฅผ ์ง„ํ–‰ํ•˜๊ธฐ์ „ ์–ด๋–ค ๊ธฐ๋Šฅ์ด ํ•„์š”ํ•œ์ง€์— ๋Œ€ํ•ด์„œ ๋‚˜ ํ˜ผ์ž ๋ฆฌ์ŠคํŠธ์—…์„ ํ•ด๋ณด์•˜๋‹ค,

  1. ๋ฉด์ ‘์ž๊ฐ€ ๋ณด์—ฌ์•ผ๋งŒ ํ•œ๋‹ค. (media ๊ธฐ๋Šฅ์„ ์—ฐ๊ฒฐํ•œ๋‹ค.)
  2. ๋…นํ™” ์‹œ์ž‘ ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด์„œ ๋…นํ™”๋ฅผ ์‹œ์ž‘ํ•  ์ˆ˜ ์žˆ๋‹ค.
  3. ๋…นํ™” ์ข…๋ฃŒ ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด์„œ ๋…นํ™”๋ฅผ ์ข…๋ฃŒํ•  ์ˆ˜ ์žˆ๋‹ค.
  4. ํ•ด๋‹น ํŽ˜์ด์ง€์—์„œ ์ด๋™ํ•˜์—ฌ ๋‹ค๋ฅธ ํŽ˜์ด์ง€๋กœ ์ด๋™ํ• ๋•Œ, ๋…นํ™”, ๋…น์Œ ๋“ฑ์˜ ๊ธฐ๋Šฅ์ด ์ค‘์ง€ ๋˜์–ด์•ผํ•œ๋‹ค.

์šฐ์„  ํ•„์š”ํ•œ ๊ธฐ๋Šฅ์— ๋Œ€ํ•ด์„œ ์ •ํ–ˆ์œผ๋‹ˆ ํ•˜๋‚˜ํ•˜๋‚˜ ํ•ด๊ฒฐํ•ด๋ณด๋ฉฐ ์ดํ•ดํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

Media Capture and Streams API์— ๋Œ€ํ•ด์„œ

Media Capture and Streams API๋Š” ์›น ๋ธŒ๋ผ์šฐ์ €์—์„œ ์˜ค๋””์˜ค ๋ฐ ๋น„๋””์˜ค ๋ฏธ๋””์–ด๋ฅผ ์บก์ฒ˜ํ•˜๊ณ , ์กฐ์ž‘ํ•˜๋ฉฐ, ์ „์†กํ•˜๋Š” ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๋Š” ๊ฐ•๋ ฅํ•œ API์ž…๋‹ˆ๋‹ค. ์ด API๋Š” ์ผ๋ฐ˜์ ์œผ๋กœ "getUserMedia", "MediaStream", "MediaStreamTrack" ๋“ฑ์˜ JavaScript ์ธํ„ฐํŽ˜์ด์Šค๋กœ ๊ตฌ์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ฃผ์š” ๊ธฐ๋Šฅ๊ณผ ์‚ฌ์šฉ ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ์„ค๋ช…ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

getUserMedia

navigator.mediaDevices.getUserMedia ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์‚ฌ์šฉ์ž์˜ ์นด๋ฉ”๋ผ์™€ ๋งˆ์ดํฌ ๊ฐ™์€ ๋ฏธ๋””์–ด ์ž…๋ ฅ ์žฅ์น˜์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๋ฉ”์„œ๋“œ๋Š” MediaStream ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋ฉฐ, ์ด ๊ฐ์ฒด๋Š” ์˜ค๋””์˜ค์™€ ๋น„๋””์˜ค ํŠธ๋ž™์„ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž์˜ ๋ช…์‹œ์ ์ธ ํ—ˆ๊ฐ€๊ฐ€ ํ•„์š”ํ•˜๊ธฐ ๋•Œ๋ฌธ์—, ์ด API๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ๋Š” ๋ณด์•ˆ ๋ฌธ์ œ์™€ ์‚ฌ์šฉ์ž ๊ฐœ์ธ์ •๋ณด ๋ณดํ˜ธ๋ฅผ ๊ณ ๋ คํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

MediaStream

MediaStream ๊ฐ์ฒด๋Š” ํ•˜๋‚˜ ์ด์ƒ์˜ MediaStreamTrack(์˜ค๋””์˜ค ๋˜๋Š” ๋น„๋””์˜ค ํŠธ๋ž™)์„ ํฌํ•จํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•ด ์˜ค๋””์˜ค ๋ฐ ๋น„๋””์˜ค ์ŠคํŠธ๋ฆผ์„ HTML์˜ <audio>๋‚˜ <video>์š”์†Œ์— ์—ฐ๊ฒฐํ•˜๊ฑฐ๋‚˜, ์›นRTC(Web Real-Time Communications)๋ฅผ ํ†ตํ•ด ๋„คํŠธ์›Œํฌ๋กœ ์ „์†กํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. MediaStreamTrack:

MediaStream์˜ ๊ฐ ํŠธ๋ž™์€ MediaStreamTrack ๊ฐ์ฒด๋กœ ํ‘œํ˜„๋ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ๊ฐœ๋ณ„ ์˜ค๋””์˜ค๋‚˜ ๋น„๋””์˜ค ํŠธ๋ž™์˜ ์†์„ฑ์„ ์กฐ์ ˆํ•˜๊ฑฐ๋‚˜, ํŠธ๋ž™์„ ํ™œ์„ฑํ™” ๋˜๋Š” ๋น„ํ™œ์„ฑํ™”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Mdn Media Stream

getUserMedia()๋ฅผ ํ†ตํ•ด mediaDevices์— ์—ฐ๊ฒฐ

getUserMedia() ๋ฉ”์„œ๋“œ๋Š” MediaDevices ์ธํ„ฐํŽ˜์ด์Šค์˜ ์ผ๋ถ€๋กœ, ์‚ฌ์šฉ์ž์˜ ์นด๋ฉ”๋ผ, ๋งˆ์ดํฌ ๋“ฑ๊ณผ ๊ฐ™์€ ๋ฏธ๋””์–ด ์ž…๋ ฅ ์žฅ์น˜์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์ด ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด ์‹ค์‹œ๊ฐ„์œผ๋กœ ์˜ค๋””์˜ค์™€ ๋น„๋””์˜ค ์ŠคํŠธ๋ฆผ์„ ์บก์ฒ˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์š”์ฒญ๋ฐฉ๋ฒ•์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

navigator.mediaDevices.getUserMedia() ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ๋ฏธ๋””์–ด ์ž…๋ ฅ ์žฅ์น˜์— ์ ‘๊ทผ์„ ์š”์ฒญํ•ฉ๋‹ˆ๋‹ค. ์ด ํ•จ์ˆ˜๋Š” constraints ๊ฐ์ฒด๋ฅผ ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ ๋ฐ›์œผ๋ฉฐ, ์ด ๊ฐ์ฒด๋ฅผ ํ†ตํ•ด ํ•„์š”ํ•œ ๋ฏธ๋””์–ด ์œ ํ˜•(์˜ค๋””์˜ค, ๋น„๋””์˜ค) ๋ฐ ๊ธฐํƒ€ ์„ค์ •์„ ์ง€์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

async function getMedia() {
	try {
		const stream = await navigator.mediaDevices.getUserMedia({
			audio: true,
			video: true,
		});
		// ๋ฏธ๋””์–ด ์ŠคํŠธ๋ฆผ ์‚ฌ์šฉ
		// ์˜ˆ: ๋น„๋””์˜ค ์š”์†Œ์— ์ŠคํŠธ๋ฆผ ์—ฐ๊ฒฐ
		// ์ธ์ž๋กœ ๋“ค์–ด๊ฐ€๋Š” ๊ฐ’์ด constraint(์กฐ๊ฑด)์ด ๋ฉ๋‹ˆ๋‹ค.
		document.querySelector("video").srcObject = stream;
	} catch (error) {
		console.error("๋ฏธ๋””์–ด ์ ‘๊ทผ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", error);
	}
}

getMedia();

์ด๋•Œ ๋น„๋™๊ธฐ๋กœ ๋™์ž‘ํ•˜๋Š” navigator.mediaDevices.getUserMedia(constraints)๋ฅผ ํ†ตํ•ด์„œ stream์„ ๋ฐ›์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ด ๊ณผ์ •์„ try catch๋กœ ์˜ค๋ฅ˜๋ฅผ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

๊ทธ ์ดํ›„ ์„ ์–ธํ•œ video ํƒœ๊ทธ๋ฅผ ํ†ตํ•ด์„œ stream ์„ ์†ก์ถœํ•  ์ˆ˜ ์žˆ๋„๋ก ์—ฐ๊ฒฐํ•ฉ๋‹ˆ๋‹ค. ํ•ด๋‹น ๊ณผ์ •์€ ref๋กœ ๋Œ€์ฒดํ•˜์—ฌ ์ง„ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋ž˜์„œ ์ €๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด getMedia ํ•จ์ˆ˜๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

const getMedia = async () => {
	try {
		const constraints = {
			audio: {
				echoCancellation: { exact: true },
			},
			video: {
				width: 1280,
				height: 720,
			},
		};
		const mediaStream = await navigator.mediaDevices.getUserMedia(constraints);
		setStream(mediaStream); // ํ•ด๋‹น stream์€ ์•„๋ž˜์˜ recorder์—์„œ๋„ ๋™์ผํ•˜๊ฒŒ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.
		if (mirrorVideoRef.current) {
			// ํ˜„์žฌ ํŽ˜์ด์ง€์—์„œ ๊ฑฐ์šธ ์—ญํ• ์„ ํ•  video ํƒœ๊ทธ์ž…๋‹ˆ๋‹ค.
			mirrorVideoRef.current.srcObject = mediaStream;
		}
	} catch (e) {
		console.log(`ํ˜„์žฌ ๋งˆ์ดํฌ์™€ ์นด๋ฉ”๋ผ๊ฐ€ ์—ฐ๊ฒฐ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค`);
	}
};

๋‹ค์Œ๊ณผ ๊ฐ™์ด mediaStream์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

์ „์ฒด์ ์ธ ์ƒํƒœ์— ๋Œ€ํ•œ ์„ค๋ช… (state์™€ ref์š”์†Œ)

์ œ๊ฐ€ ์ •์˜ํ•œ 5๊ฐœ์˜ ์ƒํƒœ์™€ ์ฐธ์กฐ๋ฅผ ์ค‘์‹ฌ์œผ๋กœ ์„ค๋ช… ํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

	// MediaStream์„ ๋ฐ›์•„์„œ ์—ฐ๊ฒฐํ•จ, ๋ชจ๋“  ref์— audio, video๋“ฑ web Api๋ฅผ ์—ฐ๊ฒฐํ•˜๋Š” ์—ญํ• 
	// ํ•ด๋‹น ๊ฐ’์ด null์ด ๋œ๋‹ค๋ฉด ์—ฐ๊ฒฐ์ด ์ข…๋ฃŒ๋˜์—ˆ์Œ์„ ์˜๋ฏธ
  const [stream, setStream] = useState<MediaStream | null>(null);
  	// ๋…นํ™” ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•˜๋Š” state, ํ•ด๋‹น ํŽ˜์ด์ง€์—์„œ ํญ๋„“๊ฒŒ ์‚ฌ์šฉ๋  ์˜ˆ์ •
  const [recording, setRecording] = useState(false);
	// ๋…นํ™”๋œ ๊ฒฐ๊ณผ๋ฌผ์ด ์ €์žฅ๋ฉ๋‹ˆ๋‹ค. type์€ Blob[] ์ž…๋‹ˆ๋‹ค.
  const [recordedBlobs, setRecordedBlobs] = useState<Blob[]>([]);

	// ํ•ต์‹ฌ์ด ๋˜๋Š” ๋‚ด์šฉ์ž…๋‹ˆ๋‹ค. ์˜ค๋กœ์ง€ "๊ฑฐ์šธ"์—ญํ• ๋งŒ์„ ์ˆ˜ํ–‰ํ•˜๋ฉฐ, ๋งˆ์น˜ ํ™”๋ฉด์ด ๋…นํ™”๋˜๋Š” ๋“ฏํ•œ UX๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
  const mirrorVideoRef = useRef<HTMLVideoElement>(null);
	// dom๋‚ด๋ถ€์—์„  ๋ณด์—ฌ์ง€์ง€ ์•Š์ง€๋งŒ, ๋‚ด๋ถ€์—์„œ "๋…นํ™”"์— ๋Œ€ํ•œ ๊ธฐ๋Šฅ๋งŒ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.
  const mediaRecorderRef = useRef<MediaRecorder | null>(null);

stream

media ์—ฐ๊ฒฐ์— ์žˆ์–ด์„œ ๊ฐ€์žฅ ์ค‘์‹ฌ์ด ๋˜๋Š” ์—ญํ• ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.

์œ„์—์„œ ์„ค๋ช…ํ•œ getUserMedia๋ฅผ ํ†ตํ•ด์„œ mediaStream ์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. ํ•ด๋‹น ๊ฐ’์ด ์„ค์ •๋˜์—ˆ๋‹ค๋Š” ์˜๋ฏธ๋Š”, ํ•ด๋‹น ์ปดํฌ๋„ŒํŠธ์™€ ์„œ๋น„์Šค๋Š” ํ˜„ ์‹œ์ ๋ถ€ํ„ฐ web API๋ฅผ ํ†ตํ•ด ์œ ์ €์˜ audio์™€ camera๋ฅผ ์—ฐ๊ฒฐ๋ฐ›์•˜๋‹ค๋Š”๊ฒƒ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค. ๋งŒ์•ฝ ์—ฌ๊ธฐ์„œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์‹œ error๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

๋˜ํ•œ ๋ฐ”๋กœ ์•„๋žซ์ค„์—์„œ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ฐจํ›„ ์„ค๋ช…ํ•  mirrorVideo์—๋„ ํ•ด๋‹น stream์„ ์—ฐ๊ฒฐํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด videoํƒœ๊ทธ๋ฅผ ํ†ตํ•ด camera์˜ ์ž…๋ ฅ๊ฐ’์ด ์†ก์ถœ๋ฉ๋‹ˆ๋‹ค. ์•„๋ž˜๋Š” record๋ถ€๋ถ„์—์„œ mediaRecorderRef๊ฐ€ ์ดˆ๊ธฐํ™” ๋˜๋Š” ๋ถ€๋ถ„์ž…๋‹ˆ๋‹ค. ์ด ๋˜ํ•œ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ mimeType์„ ์„ค์ •๋ฐ›๊ธด ํ•˜์ง€๋งŒ, ์ด ์—ญ์‹œ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ๋™์ผํ•œ stream์ด ์—ฐ๊ฒฐ๋œ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

react ์ƒ๋ช…์ฃผ๊ธฐ๋ฅผ ์ด์šฉํ•œ stream ์ข…๋ฃŒ ์„ค์ •

๋‹ค์Œ์€ ์ œ๊ฐ€ ํ”„๋กœ์ ํŠธ์—์„œ ์‚ฌ์šฉํ•œ useEffect๋ฅผ ํ†ตํ•ด์„œ stream ์„ ์„ค์ •ํ•œ ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค.

useEffect์˜ ์ฒซ ๋ฒˆ์งธ ์ธ์ž๋Š” ์‹คํ–‰ํ•  side effect๋ฅผ ๋‹ด์€ ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค. ์ด ์˜ˆ์ œ์—์„œ๋Š” ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋งˆ์šดํŠธ๋˜์—ˆ์„ ๋•Œ, ์ฆ‰ ์ฒ˜์Œ ๋ Œ๋”๋ง๋  ๋•Œ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. if (!stream) { void getMedia(); }: ์ด ์กฐ๊ฑด๋ฌธ์€ stream ์ƒํƒœ๊ฐ€ null์ธ ๊ฒฝ์šฐ, ์ฆ‰ ์•„์ง ๋ฏธ๋””์–ด ์ŠคํŠธ๋ฆผ์ด ์„ค์ •๋˜์ง€ ์•Š์•˜์„ ๋•Œ getMedia ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค. getMedia ํ•จ์ˆ˜๋Š” ๋ฏธ๋””์–ด ์žฅ์น˜์— ๋Œ€ํ•œ ์ ‘๊ทผ์„ ์š”์ฒญํ•˜๊ณ , ์„ฑ๊ณต์ ์œผ๋กœ ์ ‘๊ทผ์ด ์ด๋ฃจ์–ด์ง€๋ฉด stream ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค.

useEffect ๋‚ด์—์„œ return ํ•จ์ˆ˜๋Š” ์ปดํฌ๋„ŒํŠธ์˜ ์–ธ๋งˆ์šดํŠธ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. ์ด ํ•จ์ˆ˜๋Š” ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์–ธ๋งˆ์šดํŠธ๋  ๋•Œ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. return () => { ... } ์ด ๋กœ์ง์€ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์–ธ๋งˆ์šดํŠธ๋˜๊ธฐ ์ „์— ์‹คํ–‰๋˜๋ฉฐ, stream ์ƒํƒœ์— ํ• ๋‹น๋œ ๋ฏธ๋””์–ด ์ŠคํŠธ๋ฆผ์˜ ๊ฐ ํŠธ๋ž™์„ ์ค‘์ง€(stop())ํ•ฉ๋‹ˆ๋‹ค. ํ•ด๋‹น ํ•จ์ˆ˜๋Š” stream์„ ๋”์ด์ƒ ์‚ฌ์šฉํ•˜์ง€ ์•Š์•„์•ผ ํ• ๋•Œ, stream ์—ฐ๊ฒฐ์„ ์ข…๋ฃŒํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. ์ฆ‰ ์ปดํฌ๋„ŒํŠธ๊ฐ€ DOM์—์„œ ์ œ๊ฑฐ๋  ๋•Œ useEffect ๋‚ด์˜ ์ •๋ฆฌ ํ•จ์ˆ˜๊ฐ€ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. ์ด๋•Œ ๋ฏธ๋””์–ด ์ŠคํŠธ๋ฆผ์˜ ํŠธ๋ž™๋“ค์„ ์ค‘์ง€์‹œํ‚ค๋Š” ์ž‘์—…์„ ํ†ตํ•ด ์ž์›์„ ํ•ด์ œํ•˜๊ณ  ํ•„์š”ํ•œ ๊ฒฝ์šฐ๊ฐ€ ์•„๋‹ˆ๋ผ๋ฉด ์นด๋ฉ”๋ผ๋ฅผ ์ข…๋ฃŒํ•˜์—ฌ ์ข€๋” ์‹ ๋ขฐ๊ฐ€ ๊ฐ€๋Š” UX๋ฅผ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. ์œ ์ €๋Š” ์–ธ์ œ๋“ ์ง€ ํŽ˜์ด์ง€๋ฅผ ์ดํƒˆํ•˜๊ฑฐ๋‚˜ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์–ธ๋งˆ์šดํŠธ ๋จ์„ ์œ ๋„ํ•˜๋Š” ์–ด๋–ค ํ–‰๋™์ด๋ผ๋„ ์ทจํ•˜๊ฒŒ ๋œ๋‹ค๋ฉด, ์นด๋ฉ”๋ผ๋ฅผ ๋น„๋กฏํ•œ ๋ชจ๋“  ๋ฏธ๋””์–ด ์ŠคํŠธ๋ฆผ์„ ์ •์ง€ํ•  ์ˆ˜ ์ž‡์Šต๋‹ˆ๋‹ค.

record

const [recording, setRecording] = useState(false);

ํ”„๋กœ์ ํŠธ ๋‚ด๋ถ€์—์„  record์ƒํƒœ์— ๋”ฐ๋ผ UI ๋ฐ ๊ธฐ๋Šฅ์ด ๋ณ€๊ฒฝ๋ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ๊ฐ€์žฅ ํ•„์ˆ˜๋กœ ์ง€์›๋˜์–ด์•ผํ•˜๋Š” ๊ธฐ๋Šฅ์œผ๋กœ ํŒ๋‹จํ•˜๊ณ , ๋‹ค์Œ๊ณผ ๊ฐ™์ด startํ•˜๋Š” ํ•จ์ˆ˜์˜ ๊ฒฝ์šฐ true๋กœ ์„ ์–ธํ•˜๊ฑฐ๋‚˜

๋‹ค์Œ์„ ํ†ตํ•ด์„œ false๋กœ record ์—ฌ๋ถ€๋ฅผ ํŒ๋‹จํ•ฉ๋‹ˆ๋‹ค.

recordBlobs

const [recordedBlobs, setRecordedBlobs] = useState<Blob[]>([]);

Blob (Binary Large Object) ๊ฐ์ฒด๋Š” ๋ฐ”์ด๋„ˆ๋ฆฌ ๋ฐ์ดํ„ฐ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋ถˆ๋ณ€(immutable) ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค. ์›น ๊ฐœ๋ฐœ์—์„œ Blob์€ ์ฃผ๋กœ ํŒŒ์ผ๊ณผ ๊ฐ™์€ ๋Œ€์šฉ๋Ÿ‰์˜ ์›์‹œ ๋ฐ์ดํ„ฐ(raw data)๋ฅผ ๋‹ค๋ฃจ๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. Blob ๊ฐ์ฒด๋Š” ์ด๋ฏธ์ง€, ์‚ฌ์šด๋“œ ํŒŒ์ผ, ๋น„๋””์˜ค ํŒŒ์ผ๊ณผ ๊ฐ™์€ ๋ฉ€ํ‹ฐ๋ฏธ๋””์–ด ๋ฐ์ดํ„ฐ ๋˜๋Š” ๋Œ€์šฉ๋Ÿ‰ ํ…์ŠคํŠธ ํŒŒ์ผ ๋“ฑ์„ ๋‚˜ํƒ€๋‚ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์•„๋ž˜์˜ ๋…นํ™”๋ฅผ ํ†ต์ œํ• ๋•Œ, ๋‹ค์Œ์˜ setRecordedBlobs๋ฅผ ํ†ตํ•ด์„œ ๋…นํ™”๋œ ์˜์ƒ์„ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.

mirrorVideoRef

์ €๋Š” ํ•ด๋‹น ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด mirrorVideoRef์™€ mediaRecorderRef ๋‘๊ฐ€์ง€๋ฅผ ์„ ์–ธํ–ˆ์Šต๋‹ˆ๋‹ค.

์ฒ˜์Œ mediaStream API์— ๋Œ€ํ•ด์„œ ํ•™์Šตํ• ๋•Œ๋Š” ํ•˜๋‚˜์˜ video tag๋‚ด๋ถ€์—์„œ ์ดฌ์˜๊ณผ ์ถœ๋ ฅ, ๋…นํ™”๋ฅผ ๋ชจ๋‘ ์ง„ํ–‰ํ•˜๋Š” ์ค„ ์•Œ์•˜์Šต๋‹ˆ๋‹ค๋งŒ, ํ•ด๋‹น ๋ฐฉ์‹์œผ๋กœ ์ง„ํ–‰ํ•˜๊ฒŒ ๋˜๋ฉด, ๋…นํ™”์Œ์งˆ์ด ๊นจ์ง€๋ฉฐ echo๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.

๊ทธ์— ๋”ฐ๋ผ ์ €๋Š” ๋‘๊ฐ€์ง€ mirrorVideoRef์™€ mediaRecorderRef dom ์š”์†Œ๋ฅผ ์„ ์–ธํ•จ์œผ๋กœ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ณ ์ž ํ–ˆ์Šต๋‹ˆ๋‹ค.

์ฒ˜์Œ useRef๋กœ ์„ ์–ธ๋ ๋•Œ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ํ•ด๋‹น ๋ณ€์ˆ˜๋Š” <video> ํƒœ๊ทธ์— ๋Œ€ํ•œ ์ฐธ์กฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. useRef<HTMLVideoElement>(null)์€ mirrorVideoRef๊ฐ€ HTML์˜ <video> ์š”์†Œ๋ฅผ ์ฐธ์กฐํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋  ์ˆ˜ ์žˆ์Œ์„ ๋ช…์‹œํ•ฉ๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์„œ <HTMLVideoElement>๋Š” TypeScript์˜ ํƒ€์ž… ์ฃผ์„์œผ๋กœ, ์ฐธ์กฐ๋˜๋Š” ์š”์†Œ๊ฐ€ <video> ํƒœ๊ทธ์ž„์„ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค.

๋˜ํ•œ ํ•จ์ˆ˜ getMedia()๋ฅผ ํ†ตํ•ด์„œ ํ˜ธ์ถœ๋˜์–ด ์ฒ˜์Œ์œผ๋กœ stream ๊ฐ์ฒด๋ฅผ ๋ฐ›์•„

mirrorVideoRef.current.srcObject = mediaStream;

๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ดˆ๊ธฐํ™” ๋ฉ๋‹ˆ๋‹ค. ํ•ด๋‹น ๊ฐ’์€ ์ฐจํ›„ ์•„๋ž˜ ๋ฐ˜ํ™˜๊ฐ’์ธ

video ํƒœ๊ทธ ๋‚ด๋ถ€์—์„œ ref์— value๋กœ ์‚ฌ์šฉ๋˜์–ด ๊ฑฐ์šธ๋กœ์„œ ๊ธฐ๋Šฅํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ์•„๋ž˜์˜ ์†์„ฑ์ค‘ ๋ˆˆ์— ๋„๋Š”๊ฒƒ์€ 3๊ฐ€์ง€๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

  1. "autoPlay" : ํ•ด๋‹น ์˜ต์…˜์€ videoํƒœ๊ทธ๊ฐ€ stream์—ฐ๊ฒฐ ์ดํ›„ ๊ฑฐ์šธ๊ณผ ๊ฐ™์ด ๋ฌดํ•œํžˆ ๋ฐ˜๋ณต๋˜๋Š” ๊ธฐ๋Šฅ์„ ์ˆ˜ํ–‰ํ•˜๊ธฐ ์œ„ํ•ด์„œ ์‚ฌ์šฉ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
  2. "muted" : stream ์˜ต์…˜์„ ๋ฐ›์„๋•Œ๋Š” audio๊ฐ’์ด true์˜€์ง€๋งŒ ์—ฌ๊ธฐ์„  muted์ฒ˜๋ฆฌ๋˜์–ด ๋ถˆํ•„์š”ํ•œ echo ์ฒ˜๋ฆฌ๋ฅผ ๋ง‰์•˜์Šต๋‹ˆ๋‹ค.
  3. "transform: scaleX(-1); : ํ•ด๋‹น ์˜ต์…˜์€ videoํƒœ๊ทธ๊ฐ€ ๊ฑฐ์šธ์ฒ˜๋Ÿผ ๊ธฐ๋Šฅํ•˜๊ธฐ ์œ„ํ•ด ์ถœ๋ ฅ๋ฌผ์„ ์ขŒ์šฐ ๋ฐ˜๋Œ€๋กœ ๋ณด์ด๋„๋ก ๋ฐ˜์˜ํ–ˆ์Šต๋‹ˆ๋‹ค.

์ฆ‰ ์—ฌ๊ธฐ์„œ mirrorVideoRef๋Š” ๊ฑฐ์šธ ์—ญํ• ์„ ์ˆ˜ํ–‰ํ•˜๊ธฐ ์œ„ํ•ด ์„ ์–ธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค

mediaRecorderRef

์•ž์„œ์„œ ์„ ์–ธํ•œ mirrorVideoRef์™€๋Š” ๋‹ค๋ฅด๊ฒŒ mediaRecord ๊ธฐ๋Šฅ์„ ์œ„ํ•ด์„œ ์„ ์–ธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

const mediaRecorderRef = (useRef < MediaRecorder) | (null > null);

ํ•ด๋‹น ์ฝ”๋“œ๋Š” MediaRecorder ๊ฐ์ฒด์— ๋Œ€ํ•œ ์ฐธ์กฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. MediaRecorder๋Š” ๋ฏธ๋””์–ด ์ŠคํŠธ๋ฆผ์„ ๋…นํ™”ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋˜๋Š” ์›น API์˜ ์ผ๋ถ€์ž…๋‹ˆ๋‹ค. useRef<MediaRecorder | null>(null)๋Š” mediaRecorderRef๊ฐ€ MediaRecorder ๊ฐ์ฒด ๋˜๋Š” null์„ ์ฐธ์กฐํ•  ์ˆ˜ ์žˆ์Œ์„ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค. ์ดˆ๊ธฐ ๊ฐ’์€ null์ž…๋‹ˆ๋‹ค. ์ด ref๋Š” ๋‚˜์ค‘์— MediaRecorder ๊ฐ์ฒด์˜ ์ธ์Šคํ„ด์Šค๋กœ ์„ค์ •๋  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ด๋ฅผ ํ†ตํ•ด ๋…นํ™” ์ œ์–ด(์‹œ์ž‘, ์ค‘์ง€ ๋“ฑ)์— ์‚ฌ์šฉ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ฐจํ›„์— ์„ ์–ธํ•  ๋…นํ™”์‹œ์ž‘, ํ˜น์€ ๋…นํ™” ์ค‘์ง€์— ๋Œ€ํ•œ ํ•จ์ˆ˜์—์„œ ์ฐธ์กฐ๋˜๊ธฐ ์œ„ํ•ด์„œ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

// ๋…นํ™”ํ•˜๋Š” ํ•จ์ˆ˜
  const handleStartRecording = () => {
    setRecordedBlobs([]);
    try {
      mediaRecorderRef.current = new MediaRecorder(stream as MediaStream, {
        mimeType: selectedMimeType,
      });
      mediaRecorderRef.current.ondataavailable = (event) => {
        if (event.data && event.data.size > 0) {
          setRecordedBlobs((prev) => [...prev, event.data]);
        }
      };
      mediaRecorderRef.current.start();
      setRecording(true);
    } catch (e) {
      console.log(`MediaRecorder error`);
    }
  };

ํ•ด๋‹น ์ฝ”๋“œ๋Š” ๊ต‰์žฅํžˆ ํฅ๋ฏธ๋กญ์Šต๋‹ˆ๋‹ค. ์šฐ์„  ์ด์ „์— ์‚ฌ์šฉํ•œ stream state๊ฐ€ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ ondataavailable๋Š” MediaRecorder ์ธ์Šคํ„ด์Šค์—์„œ ๋ฐœ์ƒํ•˜๋Š” ์ด๋ฒคํŠธ๋กœ, ๋…นํ™”๋œ ๋ฏธ๋””์–ด ๋ฐ์ดํ„ฐ๊ฐ€ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•  ๋•Œ ํŠธ๋ฆฌ๊ฑฐ๋ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ํŠธ๋ฆฌ๊ฑฐ ๋˜๋Š” event์— ๋งž์ถ”์–ด setRecorderedBlob๋˜๋ฉฐ ๋…นํ™”๋œ ๊ฒฐ๊ณผ๋ฅผ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. (๋…นํ™”์ข…๋ฃŒ๋ฒ„ํŠผ์„ clickํ–ˆ์„๋•Œ event๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.)

mdn MediaStream ondataavailable

๊ฒฐ๊ณผ

์„ฑ๊ณต์ ์œผ๋กœ ๋…นํ™”๊ฐ€ ์ž˜ ์ง„ํ–‰๋จ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

์ถ”๊ฐ€์ ์œผ๋กœ ์ง„ํ–‰ํ•ด์•ผํ•  ํ…Œ์Šคํฌ

  1. stream ์ •๋ณด๋ฅผ ์ €์žฅ(server)ํ•˜๋Š” ๋กœ์ง์— ๋Œ€ํ•œ ์ดํ•ด (pre-signed url)
  2. bitrate ์„ค์ •์— ๋Œ€ํ•œ ํ•™์Šต ์žฅํฌ๋‹˜์ด ๋‚จ๊ฒจ์ฃผ์‹  ์ •๋ณด๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ํ•™์Šต ์ง„ํ–‰์ด ํ•„์š”

ํ•ด๋‹น ์ž‘์—…๊ณผ์ •์— ๋Œ€ํ•œ ์ž์„ธํ•œ ์ฝ”๋“œ๋ฅผ ํ™•์ธํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ์—ฌ๊ธฐ์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ยฉ 2024 Adultlee. All rights reserved.Made with โค by ์ด์„ฑ์ธ