Project

ideal idea 회고

hid1 2023. 4. 3. 14:50

 

 

ideal idea

 

ideal idea는 최대 4명의 사용자들이 실시간으로 캔버스를 공유하며 이상적인 아이디어를 창출할 수 있는 서비스이다.

이 프로젝트에 대해 생각하게 된 계기는 친구들과 공모전을 준비할 때였다. 함께 스토리보드를 작성하기로 하여 이 프로젝트처럼 실시간으로 함께 그림을 그릴 수 있는 사이트를 찾아보니 아래에 있는 Aggie라는 사이트를 찾아 함께 작성하였다.

 

 

Aggie.io by Magma

Draw a picture together with your friends in real-time over the internet in your browser

aggie.io

 

사이트를 실제로 이용하면서 실시간 웹 애플리케이션으로 메시지를 보내 채팅을 만드는 예제들을 몇 번 보았지만 이렇게 그림을 함께 공유하는 기능이 나에게 더욱 흥미롭고 재밌게 작업할 수 있을 것 같아 프로젝트를 시작하게 되었다.

 

 

디자인

 

ideal idea라는 문구를 보았을 때 생각난 단어는 '우주'였다. 우주의 느낌을 날 수 있는 어두운 네이비 색상과 포인트 컬러감을 줄 수 있는 분홍 색상을 주 색상으로 정하였다.

#f76597 #595B83  #333456 #060930

334563 3#3334 #56 3 456#333456

프로젝트에 사용한 아이콘은 iconmonstr사이트에서 SVG 파일로 다운받아 사용하였다.

폰트는 고딕체의 정적인 분위기보다는 귀엽고 발랄한 손글씨의 느낌을 살짝 주고 싶어 찾아보니 우리에게 익숙한 폰트인 메이플스토리 서체가 마음에 들어 선택하였다.

 

서체 | 미디어 | 메이플스토리

 

maplestory.nexon.com

 

 

개발

 

Front-End : Next.js, Typescript, Tailwind CSS, Socket.IO, Three.js

Back-End : Nest.js, Socket.IO

 

프론트엔드에서는 Next.js를 통해 프로젝트를 만들었다. Next.js를 선택한 이유는 사실 Next.js를 프로젝트를 통해 학습하려는 목적이 컸다. (다른 라이브러리,프레임워크를 굳이 우선순위로 선택할 이유도 없었다.)그리고 폴더 기반의 라우팅과 코드 스플리팅 등 다양한 기능들을 제공해주기 때문에 별도의 라이브러리 설치나 코드 작성이 필요하지 않아 개발 생산성을 높일 수 있고 SSR로 초기 로딩 속도가 빠르다는 장점을 고려하여 선택하게 되었다.

이번에 타입스크립트를 도입하게 되었다. 전에 만들었던 프로젝트의 경우 타입스크립트를 도입하기에 약간 망설임이 있었다. 그때는 굳이 적용 해야되나?라는 생각도 있었고 아직 타입스크립트에 대해 지금보다 몰랐던 시기라 일단은 자바스크립트로 작성하는데 집중하였다. 지금은 이전보다 타입스크립트에 대한 개념들을 공부하였기 때문에 직접 작성하면서 부딪혀보자는 마음이 들어 도입하였다. 실제 타입스크립트를 사용하면서 IDE를 통한 자동 완성 기능과 컴파일 단계에서의 에러 검출을 통한 개발 생산성 향상 되는 이점에 마음에 들었다. 하지만 타입을 정하거나 타입이 맞지 않는 오류 때문에 시간을 더 써야 했었다. 좀 더 멋지게 타입을 정의해서 활용할 수 있는 방법에 대해 고민하게 되었다.

또한 Tailwind CSS를 썼다. 개인적으로 Tailwind CSS를 사용하면서 제일 크게 다가왔던 장점은 클래스 네이밍을 고려하지 않아도 된다는 점이 가장 편리하고 개발 시간이 단축되는 느낌을 받았다. 하지만 자바스크립트 변수로 다이내믹 스타일을 적용하거나 config로 지정한 설정들을 자바스크립트로 가져와야 할 경우 약간 불편하였다. (내가 방법을 모를지도...)

만약 config에서 커스텀 색상을 지정하였을 경우 자바스크립트 변수로 가져와 bg-[colors.red]-100 로 가져와 사용하고 싶지만 그런 방법은 찾아보니 나오지 않아 다른 방법이 있는지 궁금하다...

 

백엔드는 Nest.js을 사용하였다. 이미 사용한 경험이 있는 Express.js를 사용할까 고민도 했지만, 우연히 Nest.js 코드를 보게 되었는데 그때 데코레이터 함수을 처음 보았다. (찾아보니  Angular.js도 데코레이터를  사용한다고 한다.) 아무튼 데코레이터 패턴에 대해 알아보고 싶기도 하고 타입스크립트도 기본적으로 설정되어 있다고 하여 선택하게 되었다. 이후에 안 사실은 Nest.js가 Express.js 기반으로 두고 있다고 한다.

사용해 본 느낌은 프로젝트 구조에 있어 견고하고 체계적인 디자인을 가지고 있다는 느낌이 강하였고 명령어로 파일들을 알아서 생성하니 편리하다는 생각이 들었다. 

다른 이야기지만 상징이 고양이인데 고양이가 귀엽다

 

 

Socket.IO를 이용한 실시간 기능 구현

 

실시간 웹 애플리케이션을 구현하기 위해 클라이언트와 서버 모두 Socket.IO 라이브러리를 사용하였다. 내가 실시간으로 공유하려는 정보는 크게 그림,채팅,사용자가 있다.

 

그림 

 

그림은 현재 캔버스의 상태와 사용자가 그리는 상태로 나누었다. 사용자가 방에 참가할 때와 도형을 그리고 난 후의 경우 현재 캔버스의 이미지를 가져와 drawImage 함수를 통해 캔버스에 그렸다. 펜이나 지우개 같은 경우는 실시간으로 공유가 가능하게 사용자가 그린 이전 위치와 현재 위치, 도구 등을 알려주어 받은 상태를 가지고 직접 작성한 drawLine 함수를 사용하여 캔버스에 그려주었다.

 

// 현재 캔버스 상태 요청 및 응답
socket.emit("canvas-state", {
        roomId,
        canvas: canvas.toDataURL(),
})

socket.on("canvas-state", (canvas: string) => {
      const img = new Image()
      img.src = canvas
      img.onload = () => {
        ctx.drawImage(img, 0, 0)
      }
})

// 사용자 드로잉 상태 요청 및 응답
socket.emit("canvas-draw", {
      prevPoint,
      currentPoint,
      roomId,
      tool,
      color,
      brushSize,
      isMarkerTool,
})

socket.on("canvas-draw", (draw: Draw) => drawLine({ ctx, ...draw }))

 

캔버스에서 도형을 그리는 방법에 대해선 글을 따로 작성하였다.

 

[HTML] canvas 캔버스 도형 그리기 (사각형, 원)

html canvas를 이용하여 드로잉 애플리케이션을 구현하던 중 도형 그리기 기능을 추가하고 싶었다. (도형 중 사각형을 위주로 설명한다.) canvas에서 사각형을 그리는 방법은 다음과 같다. const canvas =

ddd120.tistory.com

 

 

채팅

 

채팅 메시지는 사용자가 들어온 시점 이후부터만 보여주면 된다. 따라서 이전의 메시지를 가져올 필요가 없이 이후에 받은 메시지를 상태에 추가하여 보여주었다. 이 때문에 데이터베이스를 사용할 필요가 없었다.

 

  const handleSetMessage = (content: string, nickname: Nickname) => {
    setMessages((prev) => [...prev, { type: "user", nickname, content }])
    socket.emit("message", {
      roomId,
      nickname,
      content,
    })
  }

  useEffect(() => {
    socket.on("message", (message: Message) => {
      setMessages((prev) => [...prev, message])
    })

    return () => {
      socket.off("message")
    }
  }, [socket])

 

 

사용자

 

사용자의 닉네임은 임의로 토끼, 고양이, 강아지, 여우 중 하나로 정하였다.  만약 유저가 한 명도 없었을 경우 배열의 인덱스 0을 반환하고 유저가 있으면 이미 사용하는 닉네임 제외 한 배열 중 인덱스 0을 반환하였다.  사용자가 방을 들어오거나 나갈 경우 모두 사용자들에게 현재 사용자들의 상태를 전달하였다. 

 

 

setNickname(users: User[]) {
	const NICKNAME = ['토끼', '고양이', '강아지', '여우']
    
	if (!users) {
		return NICKNAME[0]
	}

	const hasNickname = users.map((user) => user.nickname)
	const nickname = NICKNAME.filter((item) => !hasNickname.some((n) => n === item),)[0]

	return nickname
}

 

 

완성된 사용자들 목록이랑 채팅 기능이다. 이외에도 메시지에 이모지를 추가할 수 있는 기능과 Clipboard API를 사용한 초대링크 복사 기능 등을 구현하였다.

 

 

[Javascipt] Clipboard API 사용

갈틱폰 사이트를 보면 방 입장시 브라우저 표시줄의 주소가 깔끔하다는 것을 볼 수 있다. 로비에서 초대 버튼을 누르면 아래와 같이 쿼리스트링을 통해 채널번호를 명시해둔 걸 볼 수 있다. 초

ddd120.tistory.com

 

이미지 저장

 

 

이미지 저장 버튼을 누르면 모달을 통해 이미지 저장에 대한 옵션을 정하여 이미지를 다운로드 할 수 있다. 배경색을 투명이나 하얀색인지를 정할 수 있고 저장명을 직접 정할 수 있게 하였다.

 

 

 

Three.js 을 사용한  3D 화면 적용

 

 

 

리액트에는 Three.js를 쉽게 구현할 수 있도록 도와주는 @react-three/drei 와 @react-three/fiber 라이브러리가 있다. 라이브러리를 사용하여 멋진 3D 화면을 구성하는데 도움이 되었다. 앞서 언급하였던 우주를 표현하였다. fiber의 Stars로 별을 추가하고 Float과 Text3D를 사용하여 글자를 둥둥 떠다니게 하였다. 태그들로 간단히 원하는 구현이 가능하여 좋았다.

 

만약 창 사이즈가 줄어들 경우 aspect이 1보다 작으면 PerspectiveCamera의 fov를 통해 더욱 멀리서 보이게 하여 나름(?) 반응형을 적용하였다. 하지만 더 좋은 방안이 있을 것 같다. 화면에 따라 글자 크기에 맞게 보여주고 싶은데 적당한 해결 방안을 찾지 못했다.

 

시작하기 버튼을 누르면 uuid로 생성한 아이디를 가진 roomId를 가진 방 페이지로 들어가 그림을 그릴 수 있다.

 

<Canvas>
	<PerspectiveCamera
			makeDefault
			manual
			position={[0, 0, 10]}
			aspect={aspect}
			fov={aspect < 1 ? 80 : 50}
			onUpdate={(c) => c.updateProjectionMatrix()}
	/>
	<Stars radius={100} count={500} fade={true} />
	<OrbitControls maxDistance={15} enableDamping={true} />
	<hemisphereLight groundColor="#ff4e8c" color="#34319c" />
	<ambientLight color="#ff4e8c" intensity={0.1} />
	<FloatText3D />
	<Html position={[0, -1, 0]} transform>
		<button onClick={handleStartClick}>시작하기</button>
	</Html>
 </Canvas>

 

 

 

 

배포

 

client는 vercel에 배포를 하였다. 처음에 github-pages로 배포를 시도하려다가 배포를 위해 따로 설정해주어야 되는게 많았다. Next.js 프로젝트는 vercel에 배포하는 걸 권장하고 있었고 나도 아주 쉽게 vercel를 통해 배포를 하였다.

 

server는 GCP의 App Engine으로 배포하였다. 처음에 배포 후 링크를 들어가서 보니 404가 뜨길래 배포가 잘못되었나?하고 당황했는데 생각해보니 루트페이지에  get 처리를 안했기 때문에 당연하게 Not Found가 뜨는 건 당연했다... 하하

또한 github actions를 통하여 메인 브랜치로 푸시 시에 App Engine으로 배포하도록 작성하였다. 

 

github actions으로 GCP app engine 배포하기

github actions을 사용하여 GCP App Engine에 서버 배포를 자동화하려 한다. jobs: deploy: runs-on: ubuntu-latest steps: - name: Checkout Repository uses: actions/checkout@v3 with: submodules: "recursive" - name: Auth to GCP uses: "google-gith

ddd120.tistory.com

 

 

 

 

 

반응형