본문 바로가기

개발이야기

[사이드 프로젝트] 실용주의 프로그래머 한 스푼을 곁들인 포토부스 개발기(n)

프로젝트 동기

회사 사수의 생일을 기념하여 여러 가지 아이디어를 생각했었다. n개월 전 부터 … 하지만 마땅한 아이디어는 나오지 않았다.😅 몇 번 시도했던게 Spline Design 활용해서 webGL사이트 만들어서 안에서 뭔가 컨텐츠를 즐길 수 있으면 좋겠다? 정도 였는데 프로그램 자체가 내 컴퓨터에서 원활하게 안돌아갔다… 회사 맥북에서만 돌아가는.. 그래서 이틀 정도 개발 해보다가 접고 다른 것들 탐색..

일주일 정도 남았을 때 아이돌 팬페이지? 같은 느낌으로 사이트 만들어서 주면 어떨까 하다가 또 너무 식상…하고 디자인에 자신이 없기 때문에 그냥 선물과 케이크와 풍선으로 만족하려다가 문득.. 며칠 전 아이유 인생네컷 포토프레임 나왔던게 생각났다. …..?

와 좀 재밌겠는데 싶어서 빠르게 제작할 수 있도록 관련 라이브러리 추려보면서 시작!

프로젝트 설명

웹캠을 활용하여 사진을 찍고 다운로드 받을 수 있는 포토부스 서비스! for나의사수🤩🍖

JJEONG'S BDAY

프로젝트 기술

React.js CSS React-Webcam html2canvas Vercel

  • React.js
    • 사용자 선택에 의해 UI가 전환되는 경우가 많고 컴포넌트화 할 수 있는 부분이 많을 것 같아서 선택했다. 결과적으로 아주 만족스러웠다. 클릭으로 바뀌는 state에 따라 컴포넌트만 갈아끼워서 웅경네컷, 웅경그레이 영역을 쉽게 전환할 수 있고 Cam영역이나 버튼 영역등 공통으로 쓰이는 부분들은 컴포넌트화해서 재사용했다.
    • 회사에서는 해당 기술을 쓰지 않아서 빨리 쓸 수 있을까 걱정했는데, 몇 번 형식만 다시 복기하니깐 바로 쓸 수 있었다. 이래서 원리 위주로 알아두는게 중요한걸까
  • React-Webcam
    • 리액트에서 바로 사용할 수 있는 웹캠 라이브러리라서 선택
  • html2canvas
    • 여러 canvas관련 라이브러리 중 가장 상위에 있는 라이브러리를 우선 선택했다. 일단 관련 글들이 많아서 빠르게 쓰기에 편할 것 같았다. object-fit을 지원하지 않는 다는 글을 보고 조금 고민했는데 해당 속성을 쓸지 미지수여서 그냥 선택했다.

컴포넌트 구조

  • 크게 Main 컴포넌트의 첫 페이지와 포토부스 기능을 하는 PhotoBooth 페이지를 넣는 이름 유무에 따라 다르게 보여주기로했다.
import { useState } from 'react'
// import './App.css'
import Main from './pages/Main'
import PhotoBooth from './pages/PhotoBooth'
function App() {
  const [name, setName] = useState('')

  return (
    <>
      {/* 개발 편의를 위해 임시 주석 처리 */}
      {name ? <PhotoBooth name={name}/>: <Main setName={setName}/>}
     {/* <PhotoBooth/> */}
    </>
  )
}

export default App

  • Main페이지는 아직 타 컴포넌트를 따로 import하지는 않는다.
  • PhotoBooth.jsx는 안에 부스 모드에 따라 특정 영역에 Necut.jsx나 Photogray.jsx가 렌더링 된다.
//모드 바꾸는 함수
const changeMode  = () => {
    if(boothMode === 'Necut'){
      setBoothMode('Pho')
    }else{
      setBoothMode('Necut')
    }
    setCapturedImages([])
  }
 //포토부스.jsx일부 
 {boothMode === 'Necut' ? <Necut capturedImages={capturedImages}/> : <Photogray capturedImages={capturedImages} /> }

개발일지

최근에 팀내 마케터님과 [실용주의 프로그래머] 스터디를 진행하고 있다. 그래서 기회가 됐으니 사이드 프로젝트를 하면서 거기에서 나온 여러 가지 이론들을 실천해보기로 했다. 우선 엔지니어링 일지 쓰기 부분을 생활화 해서 매일 노션에 적었다. 물론…. 책에는 노트에 적으라고 되어있긴 했지만…

  • 1일차
    • 아이디어가 문득 떠올라서 바로 실행하기로 했다. 리액트 공부가 진도가 안나가서 리액트 활용해서 개발하기로 함
    • 메인 페이지 퍼블리싱 하다가 예광탄에 대해서 읽었던게 생각나서 구현 가능한지 간단하게 테스트 하기로 함
    • npm create vite@latest 로 간단하게 시작!
    • 기획만 빠르게 완료함
  • 2일차
    • 역시 리액트 생태계는.. 없는게 없다 웹캠 라이브러리와 html2canvas 활용해서 만들 수 있을 거라고 판단
    • 메인 1차 퍼블리싱 완료 디테일한건 시간 되면 잡아야지..
    • 웅경네컷 페이지에 예광탄 🐴쏴봤는데 구현 되는걸 확인. 앞으로 디벨롭 시키면 완성할 수 있을 듯 하다. React-Webcam과 html2canvas 영역만 빠르게 만들어 봤다.
    //웹캠 컴포넌트
      <Webcam
           audio={false}
           ref={webcamRef}
           screenshotFormat="image/jpeg"
           width={boothMode === 'Necut'? 320 : 400}
           height={boothMode === 'Necut' ? 240 : 200}
            />
       //버튼들 
              <div className={styles.cameraControler}>
                <button onClick={capture} disabled={capturedImages.length >= 4}>
                 촬영
                </button>
                <button  onClick={downloadImage}>download</button><button onClick={changeMode}>모드변경</button>
              </div>
          </div>
          
      //html2canvas 라이브러리 코드
        const downloadImage = () => {
        const element = document.getElementById('fram');
        if (element) {
          html2canvas(element).then((canvas) => {
            const link = document.createElement('a');
            link.href = canvas.toDataURL('image/png');
            link.download = 'fram.png';
            link.click();
          });
        } else {
          console.error("Element with id 'fram' not found.");
        }
      };
    
    • className 템플릿리터럴로 작성하는 것과 Props 받아와서 쓰는게 너무 오랜만이라 살짝 헷갈리는데 그래도 빨리 익숙해지는 듯 하다.
    • 캠퍼즈 깃헙 들어가서 폴더 구조 보고 좀 구경했는데 오오..
  • 3일차
    • 사진을 찍으면 해당 영역에는 찍은 사진이 들어가고 나머지 영역은 그대 빈 공간인 로직을 구현했다.
    const DefaultArea = ({ captureList }) => {
        const numElementsToRender = 4 - captureList.length;
        const remainingPhotoMeta = numElementsToRender === 0 ? [] : photoMeta.slice(-numElementsToRender);
        return (
          <>
            {remainingPhotoMeta.map((meta, i) => (
               <div className={styles[`fram${meta.idx}`]} key={i}>
                <img className={styles.themeImg} src={meta.path}/>
              </div>
            ))}
          </>
        );
      };
    
    export default function Necut({capturedImages}) {
    
      return (
        <div className={styles.framWrapper} id='fram'>
            {capturedImages.map((image, index) => (
            <div className={styles[`fram${index}`]} key={index}>
                {/* <h2>Captured Image {index + 1}:</h2> */}
                <img className={styles.framImg} src={image} alt={`Captured ${index + 1}`} />
                <img className={styles.themeImg} src={photoMeta[index].path}/>
              </div>
              
            ))}
            <DefaultArea captureList={capturedImages}/>
            <h1>웅경네컷</h1>
          </div>
      )
    }
    
    //DefaultArea : 그냥 프레임 이미지만 가지는 빈 영역 
    // const remainingPhotoMeta = numElementsToRender === 0 ? [] : photoMeta.slice(-numElementsToRender);인덱싱
    
    • 2-3시간 정도 시간 가는 줄 몰랐다. 결과물 슬슬 나오니깐 재밌다!!!
  • 4일차
    • 웅경그레이도 완성돼서 빠르게 vercel로 배포했다.
    • 근데 이미지 파일들이 다 안떠서 뭐지? 싶었다. 그냥 src에 경로로 넣어줬는데 .. 배포 히스토리를 보니깐 아예 다운로드 자체가 안된듯 싶어서 찾아봤다.

    • 번들링 하는 과정에서 import를 사용해야 경로 문제가 없을 거라고 해서 import문으로 바꿔줬다.
    • import 눕 from '../assets/눕.png'; import 미니언 from '../assets/미니언.png'; import 카페 from '../assets/카페.png'; import 곰 from '../assets/곰.png'; const photoMeta = [ { 'path': 눕, 'idx': 0 }, { 'path': 미니언, 'idx': 1 }, { 'path': 카페, 'idx': 2 }, { 'path': 곰, 'idx': 3 } ];

 

느낀점

  • 아무래도 친한 친구의 생일엔 진심인 인간이라 동기부여가 잘되고 재밌었다. 생일인 친구한테 사이드 프로젝트의 장작이 되어줘서 고맙다는 말을 전하며.. 생일축하해 🎈

웅경 생일 축하한다아아악

  • 일단 빠르게 만드는게 중요해서 ( 얼마 남지 않은 생일 이슈로.. ) 당장 생각나는 로직으로만 구현했는데 어떻게 하면 렌더링 최소화 하면서 그릴 수 있을지 고민해봐야겠다.
  • 시간 관계상 파이어베이스 못붙인게 조금 아쉽다…!