마일리지 서비스가 성공적으로 운영되던 시점
운영을 시작한 지 일주일이 지나면서 서비스가 안정적으로 자리 잡아갔다. 덕분에 나도 조금 여유를 가질 수 있었다.
그래서 이 시간을 활용해 개발 서적을 빌려 읽고, 매일 출근해서 공부하며, 코딩 테스트를 풀며 전기세를 낭비하면서 등록금을 열심히 탕진(?)하고 있었다.
웹캠네컷 아이디어의 시작
나는 맥북 포토부스를 활용해 사진이나 영상을 남기는 걸 좋아하는데, 이게 쌓이다 보니 저장 용량만 20GB가 넘어가 버렸다. 인생네컷 같은 셀프 사진관도 자주 갔는데, 친구들이 졸업한 후에는 혼자서 맥북 포토부스로 찍는 게 유일한 취미가 되어버렸다. (ㅜㅜ)
그러다 문득 떠올랐다.
'인생네컷을 브라우저에서 바로 찍고 저장할 수 있지 않을까?'
말 그대로 웹캠네컷인 셈이다.
이 아이디어 하나로 도파민이 폭발하면서 머릿속에서 다양한 아이디어가 쏟아져 나왔다.
웹 브라우저에서 사진 촬영하기
웹에서 사진을 촬영하는 방법을 먼저 찾아봤다.
역시나 못하는 게 없는 웹의 세계!
navigator.mediaDevices.getUserMedia({ video: true })
.then((stream) => {
const video = document.querySelector("video");
video.srcObject = stream;
})
.catch((err) => console.error("카메라 접근 실패:", err));
Stream API
MediaDevices 객체는 브라우저의 카메라, 마이크 같은 미디어 디바이스를 조작하는 인터페이스를 제공한다.
- Stream API를 활용하면 브라우저에서 실시간으로 오디오 및 비디오 데이터를 받을 수 있다.
- 실시간 방송을 스트리밍하는 방식과 유사하게, 비디오 데이터를 작은 단위(Chunk)로 전송하며 화면을 렌더링한다.
이를 바탕으로 React 컴포넌트에서 직접 웹캠을 활용해보았다.
import { Button } from '@/components';
import { useEffect, useRef, useState } from 'react';
const CameraCapture = () => {
const [, setPhotos] = useState<string[]>([]);
// 비디오 스트림 상태 (사용하지는 않지만 상태를 저장할 수 있다)
const [, setStreamVideo] = useState<MediaStream | null>(null);
// 웹캠 비디오와 캔버스를 참조할 Ref
const videoRef = useRef<HTMLVideoElement | null>(null);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
useEffect(() => {
openCamera();
}, []);
const openCamera = () => {
// 사용자의 카메라 접근 권한을 요청
navigator.mediaDevices
.getUserMedia({ video: true })
.then(stream => {
setStreamVideo(stream);
const video = videoRef.current;
if (video) {
video.srcObject = stream;
video.play();
}
})
.catch(error => {
console.error('Error opening camera:', error);
});
};
const capturePhoto = () => {
const canvas = canvasRef.current;
const video = videoRef.current;
if (video && canvas) {
const context = canvas.getContext('2d');
if (context) {
// 비디오 프레임을 캔버스에 그림
context.drawImage(video, 0, 0, canvas.width, canvas.height);
// 캔버스를 이미지 데이터로 변환 (Base64 형태)
const imageData = canvas.toDataURL('image/png');
// 기존 사진 배열에 추가
setPhotos(prev => [...prev, imageData]);
}
}
};
return (
<div className="flex flex-col gap-2">
{/* 웹캠 비디오 */}
<video
ref={videoRef}
width="540"
className="w-[540px] scale-x-[-1] aspect-[3/2] object-cover object-center"
/>
{/* 촬영 버튼 */}
<div className="flex flex-row justify-center gap-10">
<Button label="사진 찍기" color="gray" onClick={capturePhoto} />
</div>
{/* 캡처한 사진을 저장할 캔버스 (화면에 표시되지 않음) */}
<canvas
className="hidden"
ref={canvasRef}
width="720"
height="480"
></canvas>
</div>
);
};
export default CameraCapture;
사진 촬영이 성공적으로 구현되었다!

기능 정리 및 구현 목표
되는게 확인되었으니 이 프로젝트를 통해 실습해보고 싶은 기능을 정리했다.
- 웹 브라우저를 통한 카메라 접근
- 컴포넌트 → 이미지 변환
- Tailwind CSS 적용
- 한번쯤 써보고 싶었는데, 이번 기회가 딱이라고 생각했다.
- 한번쯤 써보고 싶었는데, 이번 기회가 딱이라고 생각했다.
추가적으로, 나중에 사진을 아카이빙하는 기능을 추가하면 백엔드 작업도 경험할 수 있겠다는 기대감이 들었다.
이미지 변환 및 저장 기능
사진은 이제 찍었으니, 이를 이미지 파일로 변환하는 기능도 필요했다.
HTML을 이미지로 변환하는 라이브러리
컴포넌트를 이미지로 변환하는 라이브러리는 여러 개가 있었다. html2canvas, dom-to-image … 등등
처음에는 html2canvas를 선택했는데, 이후 dom-to-image로 변경하게 된다. (이유는 나중에 자세히 설명하겠다.)
이미지 저장을 위해 file-saver 라이브러리를 사용했다.
import html2canvas from "html2canvas";
import saveAs from 'file-saver';
const SavePhotoPage = () => {
const { setPhotos } = usePhotosContext();
// 변환할 HTML 요소 참조
const divRef = useRef<HTMLDivElement>(null);
const handleDownload = async () => {
if (!divRef.current) return;
try {
const div = divRef.current;
const blob = await html2canvas.toBlob(div);
saveAs(blob, 'result.png');
} catch (error) {
console.error('Error converting div to image:', error);
}
};
return (
<div className="flex flex-row items-center justify-center w-full gap-10 bg-amber-50">
<Button label="다운로드" onClick={handleDownload} />
<div ref={divRef}>
<PhotoFrame />
</div>
</div>
);
};
export default SavePhotoPage;
이렇게 다운받은 나의 네컷 사진 !
뒤에 보이는 외계인 초록은 네컷을 감쌀 컴포넌트로 아무거나 넣어봤다... ((그린스크린 같네))

프레임 추가 및 사진 비율
마지막으로 프레임을 추가했다.
0.0.1 버전이므로 간단한 프레임을 제작해 적용했다.
흥미로운 점은 각 인생네컷 사진관마다 사진 비율이 모두 다르다는 것이었다. 처음에는 사진 크기 비율을 찾으려고 엄청나게 구글링했지만, 실물 사진을 보니 제각각이어서 그냥 원하는 비율로 진행했다.
피그마로 간단한 프레임을 만들어 추출한 후, 촬영한 사진이 정확한 위치에 배치되도록 구현했다.

프로젝트 첫 버전 완성!
이렇게 저장 기능까지 확인한 후, 나만의 웹캠네컷 완성!
너무 재미있어서 하루 만에 모든 기능을 구현했다.
도파민이 넘쳐서, 추가로 건디네컷(건디 늘 감사해요~)도 만들었다.
이후 업데이트에서 사용자가 원하는 프레임을 직접 업로드할 수 있도록 확장하면 재미있을 것 같다.
오늘도 성장 도파민 중독자의 하루였다. 🚀
아직 완전히 완성된 건 아니라서 진행 중에 있지만 혹시 기대가 되신다면 한번 구경오세요... ~
디스커션에서 새로운 기능이나 필터, 프레임 등의 아이디어를 전해받고 있습니다!! 이 글을 읽고 좋은 인사이트가 있다면 알려주세요 !!
GitHub - healim01/toaster: 웹캠네컷
웹캠네컷. Contribute to healim01/toaster development by creating an account on GitHub.
github.com
'개발자의 성장 도파민 기록' 카테고리의 다른 글
📸 toaster: 네컷 사진이 사라졌다!? 새로고침을 견디는 상태 저장 여정 (1) | 2025.04.12 |
---|---|
🥐 toaster: Rive를 사용해서 귀엽고 인터랙티브한 서비스 만들기 (5) | 2025.04.08 |
🏆 마일리지 장학금 신청 서비스 개발기: 한달간의 성장 일지 (11) | 2025.03.20 |
👀 개발자가 건강을 챙기는 방법: "EYE CARE" 익스텐션 프로젝트 (1) (6) | 2025.02.15 |
우아콘2024 듣고 감명받은 "우아한플레이그라운드" 제작기 따라 해보기 (2편) (0) | 2024.12.13 |