포스트

비트 캠프 파이널 프로젝트 회고록

파이널 프로젝트를 회고하며...

비트 캠프 파이널 프로젝트 회고록

기나긴 6개월이 끝나고 마지막 프로젝트를 진행한지 약 2개월이 지나 마지막 발표까지 마무리되고 수료를 했다. 프로젝트 구현에만 바빠 하지 못했던 뒷정리를 해보려한다.

일단 우리의 프로젝트를 정리하고 남겨 놓는다.

프로젝트 개요

저는 주로 여행을 가면 사진과 함께 기념품을 사오곤 했습니다.
기념품과 함께 누구와 어디를 갔는지 기록하기 딱 좋기 때문이죠.
제 여행의 추억은 누구와 “어디”를 갔는지가 중요합니다.
또한 여행지를 찾다보면 결국 돌고 돌아서 익숙한 곳만 찾게 되어 동일한 지역을 계속 방문하는 것을 피하고 싶었습니다.
그래서 “어디”에 중점을 둔 여행 기록 사이트 아이디어를 제안하고 진행하게되었습니다.

프로젝트 일정

기획 : 2024.09-
개발 : 2024.11-
(KDT 해커톤 예선 준비로 인한 프로젝트 개발이 밀림)

프로젝트 팀 구성

팀장 : 이선아(기획)
팀원 : 장혜정(프론트), 이가람(백&프론트), 황민지(백&프론트), 김주연(백), 이태정(백&프론트)

프로젝트 사용 기술

사용 언어 : java, js
front 사용 기술 : React, axios, NGINX
back 사용 기술 : SpringBoot, SpringSecurity, MyBatis, JWT, MySQL
DevOps 사용 기술 : Docker, Jenkins, 사용 API : Flicking, SweetAlert2, LoadableComponents, JavaMail, Google API

프로젝트 기능

  1. 회원가입을 통해 회원가입을 할 수 있고 로그인이 가능합니다.
  2. 여행 게시글을 등록, 수정, 삭제 할 수 있습니다.
    (제목, 사진, 지역, 내용 > 필수 입력)
  3. 게시글을 카드 리스트 형태와 지도 형태로 조회가 가능합니다.
  4. 카드 리스트에는 검색과 정렬이 가능합니다.
  5. 마이페이지에서 사용자의 프로필 사진, 닉네임, 비밀번호 변경이 가능합니다.
  6. FAQ 게시글을 통해 많은 질문에 대한 답변을 볼 수 있습니다.

아키텍쳐

  • System 아키텍쳐
  • CI/CD

맡은 부분과 트러블 슈팅

  • 맡은 부분 지도에서 스토리를 조회, 등록, 수정, 삭제하는 부분을 맡았습니다.
  • 구현하면서 알아갔던 것 백단에서 트러블 슈팅이 있었으면.. 했지만 팀원이 만들어놓은 스토리 api를 사용하고 제가 맡은 부분은 조회 부분이기에 사실 큰 어려움이 없었습니다.

하지만 프론트를 구현해가면서 고민하고 깨달아갔던 점이 있어 이를 얘기해보려합니다.

  1. SVG에 이미지 넣기
    SVG로 된 지도 path에 이미지를 넣는데에 꽤나 애를 먹었습니다.
    fill 속성에 이미지url을 넣으면 뜰줄 알았지만… 하얀 빈 화면만 나와 이유를 알아보니

    fill 속성에 대하여
    fill은 도형에 대하여 색상, 그라이언트, 패턴과 같은 색상 채우기 관련 속성만 지원하는 속성이다.
    fill에 이미지 url을 넣어봤자 유효하지 않은 속성 사용법인 것.

    그래서 알아본 결과
    defs에 clipPath, pattern을 사용해 도형에 맞게 이미지를 잘라낼 수 있음을 알게되었다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    
     <g id={gId}>
         <defs>
             <clipPath id={clipPathId}>
                 <path ref={pathRef} d={pathD} id={pathId} />
             </clipPath>
         </defs>
         <image ref={imgRef} href={imgHref} clipPath={`url(#${clipPathId})`} id={imgId} />
         <path
             fillOpacity={0}
             ref={pathRef}
             d={pathD}
             id={"p" + pathId}
             filter="url(#shadow)"
             className={`${mapStyles.province__active__filter__p}`}
             />
         <use href={`#${pathId}`} fill={`url(#${imgId})`} />
     </g>
    

    위 코드를 사용해 도형에 맞게 이미지를 잘라냈지만 중앙에 맞지 않는 이슈가 있었다. 그리하여 챗GPT를 이용해 이미지를 중앙에 맞추고 path 크기에 맞게 크기를 조정하는 알고리즘을 부탁해 적용시켜주었다.

    알고리즘 코드

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
     useEffect(() => {
             const image = new Image();
    
             image.onload = () => {
                 // 원본 이미지가 로드된 후 크기 값을 가져옴
                 setImageDimensions({
                     width: image.width,  // 원본 이미지의 width
                     height: image.height,  // 원본 이미지의 height
                 });
             };
             // 이미지 소스를 설정하여 로드 시작
             image.src = imgHref;
         }, [imgHref]);
    

    이미지가 로드 되기 전 위치 알고리즘이 실행되는 것을 막기 위해 이미지가 로드가 되는 순간 해당 이미지의 값을 설정해준다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    
     useEffect(() => {
             if (imageDimensions.width === 0 || imageDimensions.height === 0) return;
    
             const imageTag = imgRef.current;
             const pathTag = pathRef.current;
    
             const pathBox = pathTag.getBBox();
    
             let imgWidth, imgHeight, imgX, imgY;
    
             // 이미지가 정사각형일 때 크기 조정
             if (imageDimensions.width === imageDimensions.height) {
                 // 더 큰 값을 기준으로 크기 설정
                 const scaleFactor = Math.max(pathBox.width, pathBox.height) / imageDimensions.width;
    
                 imgWidth = imageDimensions.width * scaleFactor;
                 imgHeight = imageDimensions.height * scaleFactor;
    
                 // 중심 좌표를 기준으로 배치
                 imgX = pathBox.x + (pathBox.width - imgWidth) / 2;
                 imgY = pathBox.y + (pathBox.height - imgHeight) / 2;
             } else {
                 // 비율이 다른 경우 기존 로직 적용
                 if (imageDimensions.width > imageDimensions.height) {
                     imgHeight = pathBox.height;
                     imgWidth = (imageDimensions.width / imageDimensions.height) * pathBox.height;
                     imgX = pathBox.x + (pathBox.width - imgWidth) / 2;
                     imgY = pathBox.y;
                 } else {
                     imgWidth = pathBox.width;
                     imgHeight = (imageDimensions.height / imageDimensions.width) * pathBox.width;
                     imgX = pathBox.x;
                     imgY = pathBox.y + (pathBox.height - imgHeight) / 2;
                 }
             }
    
             // 이미지 속성 설정
             imageTag.setAttribute("width", imgWidth);
             imageTag.setAttribute("height", imgHeight);
             imageTag.setAttribute("x", imgX);
             imageTag.setAttribute("y", imgY);
    
         }, [imageDimensions]); // imgHref가 변경될 때마다 이미지 정보를 업데이트
    
    

    그 후 설정된 이미지의 크기와 path의 크기 경우의 수에 따라 가로, 세로 값을 조정 후 중앙에 맞춰주는 코드이다.

  2. SVG 지도 자동화
    해당 코드를 다 짜고 하나의 지역에 이미지가 들어가는 것을 확인 후 이제 모든 지역에 해당 코드를 추가 해줘야하는 순간이 왔다.
    파일을 열어보니 행정구역 별로 js 파일이 존재했고(총 17개…) SVG 태그가 지역마다 하나씩 존재했다.

    유지보수 측면이나 코드 중복이 일어날것 같아 이를 하나의 파일에서 작업 할 수 있도록 자동화하기로 했다.

    • createElement 메서드 사용하기.
      결론부터 말하자면, 실패했다.
      알아보니 createElement는 HTML 요소를 만들기 때문에 SVG요소인 defs나 clipPath를 만들면 기능이 없는 이름만 있는 빈 태그가 생성이된다.

    • JS SVG 라이브러리 D3 사용하기
      팀원 분들에게 물어보니 JS에서 SVG 라이브러리 D3를 알려줘서 사용해봤다.
      path를 만들때는 정상적으로 만들어졌지만 defs나 clipPath를 생성하니 createElement와 동일하게 빈화면이 노출되었다.
      원인은 불명이라 다른 방안을 찾기 시작했다.

    • 컴포넌트화 하기 우리의 프론트 프로젝트는 리액트로 되어있다.
      리액트의 가장 강점이라고 함은 컴포넌트라고 할 수 있지 않은가.
      위의 태그 코드를 컴포넌트화 시킨 후 필요한 d 패스를 props로 넘겨 컴포넌트를 반복하여 이미지가 채워진 지역도, 빈 지역도 정상적으로 나오는 것을 확인 할 수 있었다.

결과와 성과

프로젝트를 진행하면서 스프링의 MVC 패턴에 대해 알아보고 도메인과 서비스단의 코드 분리에 대해 공부 할 수 있었다.
다른 사람들이 짜놓은 코드를 보고 왜 이렇게 짜야하는 것인가. 더 좋은 방법은 없을까? 등
특히 로그인 부분에 대해 보안(로컬 스토리지와 쿠키, JWT 토큰의 엑세스 토큰과 리프레쉬 토큰 등) 코드의 쓰임과 그 이유를 진지하게 고민해보는데 많은 시간을 투자했다.
특히나 프로젝트 초기 구성을 어쩌다보니 짜게 되었으나 배포를 염두에 두지 않고 짜서 react와 springBoot를 하나의 프로젝트로 묶어 생성 후 axios를 통해 cors 정책 문제를 해결하고 요청을 보내는 식으로 짰었다.
추후 배포가 들어가면서 NGINX를 도입하고 react와 springBoot를 별도의 프로젝트로 분리해 자동 배포까지 연결시키며 더 넓은 프로젝트로 발전을 시켰다는 것에 만족스럽다.

향후 계획

일단 리팩토링을 해보는 것이 가장 해보고싶은 것이다.
내 파트가 아닌 다른 파트를 리팩토링하면서 코드에 대해 공부하고 더 좋은 방법을 찾고싶다.
공모전으로 인해 프로젝트를 축소하면서 넣지 못한 기능을 추가하는 작업을 하고싶다.

마무리

총 6개월의 부트캠프를 마치고 나온 프로젝트라기엔 사실 아쉬운 점이 많은게 사실이다.
개발해보고 싶었던 것 100에서 한 40 정도 해봤다?
그래도 React를 활용해 백과 프론트를 분리해봤고 배포를해서 localhost가 아닌 도메인을 통해 들어갈 수 있다는 점.
CRUD를 온전히 짜보지 못한 부분이 가장 아쉽고 Redis나 MongoDB 등 기능이 작기에 굳이 다른 용도의 DB를 추가 시켜보지 못한 것,
WebSocket을 통해 실시간 알림 서빙을 적용하지 못한 것이 아쉬움에 남는다.
앞으로의 향후 계획에 추가해 개발하는 걸 목표로 둘 예정이다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.