시작하며…

2주동안 진행했던 부트캠프 첫 프로젝트가 마무리되었다. 짧은 시간이었지만 팀원들과 오프라인으로 만나 금방 친해질 수 있었다. 새로운 사람들을 만나서 즐겁고 설랬다. 오히려 이제 겨우 친해졌는데 자주 못만난다고 생각하니 아쉬운듯 하다…

거두절미하고 부트캠프에 집중하면서 내실을 다지고 싶어 휴학까지 한 김에 부트캠프를 진행하면서의 발자취를 남겨보려고 한다. 회고록의 첫 글은 처음 진행한 토이프로젝트에 대해 주저리주저리 떠들어보려고 한다.

팀빌딩 미팅

기존보다 조금 앞당겨 진행된 팀빌딩 미팅때는 정신이 하나도 없었다. 공개SW개발자대회가 막 끝난 상황에 논문 작성, 종합설계, 학교 수업까지 챙기려다보니 정신이 반쯤 나가있었달까…? ㅎㅎ

사실 팀빌딩 미팅때는 뭘 했는지 기억이 나지 않는다. 지금 와서 팀빌딩 미팅 내용을 좀 훑어봤는데 할줄 아는 것, 다룰줄 아는 기술 스택같은것을 정리해보았었다. 솔직히 말하면 그때 강의가 잔뜩 밀려있어서 TS와는 완전 거리두고 있었던 상황이라 더욱 걱정이 되었지만, 팀원들도 다들 처음이어서 나름대로의 걱정을 가지고 있었던 것 같다.

자기소개 시간에는 MBTI를 말했는데 난 ISFJ인데 누가 INFJ를 만들어놨다 ㅋㅋ

image

이어서 팀장을 선출했는데 왜 다빈이가 뽑혔는지는 생각 안나지만 다빈이가 팀장으로 뽑혀있었다. 🙌🙌 팀장을 뽑고 팀 규칙을 만들었는데 내가 학교 일정때문에 이날은 안돼요 저날은 안돼요 했던것같다… (미안해 얘들아 ㅠㅠ 😢) 우리 조는 월, 목요일에는 오프라인으로 만나기로 했는데 한편으로는 일정을 감당할 수 있을지 걱정이 되었지만 다른 한편으로는 새로운 사람을 만난다고 생각하지 좋았다 ㅎㅎ

첫 오프라인 모임

팀빌딩 미팅 이후 다음주 월요일이 되어 첫 오프라인 미팅을 가지게 되었다. 사실 주말에 밀린 강의와 종설 회의가 있어서 토이프로젝트 진행을 거의 하지 못했는데 팀원들이 주말 사이에 개발 문화, GIT Flow 같은 내용을 작업해주었다 ㄷㄷ… 심지어 오후 2시에 만나기로 했는데 유나와 민섭이가 오전에 나와서 프로젝트 기획이랑 디자인을 피그마로 쓱싹 끝내놨다.

나는 항상 프로젝트할 때 기획과 디자인하는 것이 힘들었다. 개발만 공부하던 친구들과 기획을 하려니 다들 번뜩이는 아이디어를 내는 것을 힘들어했고(나도 마찬가지), 디자인… 이랑은 거의 벽을 쌓은 수준이라 거의 고려하지 않거나 아는 사람한테 도움을 요청하고는 했다. 그런데 기획과 디자인을 반나절만에 그것도 둘이서…? 그때는 처음 보는 친구들이었지만 내심 존경스럽기까지 했다 ㅎㅎ…

첫날은 간단한 기획과 디자인을 한 다음 다같이 저녁을 먹었다. 다들 서로 아무거나 먹어도 괜찮다며 양보를 하다가 결국 앞에 보이는 치킨집에 들어갔다. 치킨에 맥주가 빠질 수 있겠는가? 맥주 한잔 하면서 말도 트고 많이 친해졌던 것 같다. (물론 여기서 내 이미지가 좀 이상해진것같긴 하지만 ㅎㅎ;;) 여담으로 이날 헤어지고 민섭이가 12시가 넘어서까지 카톡을 안보길래 걱정했는데 그 정신으로 스터디카페에 갔다가 골아 떨어졌다고 한다. 대단한 녀석…

1차 멘토링

우리 조 멘토링 일정이 다행이 개발 초반에 배정되어서 초반에 궁금했던 내용들을 여쭤볼 수 멘토님께 여쭤볼 수 있었다. 멘토님의 피드백 중 하나는 styled component를 사용해보라는 것이었다. 아직 css도 잘 다루지 못하는데 잘 다룰 수 있을까? 라는 생각을 잠깐 했지만 props를 내려줄 수 있다는 점과 component별로 스타일링을 할 수 있다는 너무나도 큰 장점이 있어 styled component를 사용하게 되었다.(물론 유나, 은지가 스타일링은 거의 다 해줬지만…)

또한, TS 프로젝트인만큼 멘토님도 TS와 친숙해져야한다고 강조를 하셨다. 아직 강의도 제대로 안들었는데… 그래도 팀원들한테 민폐끼치지 않기 위해 틈날때마다 공부했다…

그리고 패키지 충돌 문제에 대해서도 지적해주셨다. 실제로, 이전에 진행했던 프로젝트들은 패키지 버전 차이때문에 팀원들간에 문제가 발생했던 경험이 많기 때문에 PR을 올리면 무조건 카톡으로 공유하고 pull을 받고 무조건 npm install로 패키지를 설치하고 개발을 시작하기로 약속했다. 초반에 이런 규칙을 정해놨기 때문에 이후에도 거의 conflict가 발생하지 않았던 것 같다.

그리고 멘토님이 우리조의 캘린더와 todo를 보고 지적을 하셨다. 전반적인 계획과 역할 분담이 잘 되지 않았다는 이유였다. 그래서 부랴부랴 개발 일정과 각자 todo를 작성했던 기억이 있다. 다음에는 좀더 디테일한 계획을 세우고 개발을 해야할 듯 하다…

마지막으로, db쪽 todo 작업을 같이 해보라고 하셨는데, 초반에 기본적인 crud 기능을 구현한 것 빼고는 db관련된 todo들은 사실상 민섭이가 도맡아 했던 것 같다. 물론 나중에는 BE쪽에서 해주겠지만 FE 개발자로써 DB 구조를 파악하고 request와 response의 기본적인 형태는 알고 있어야 한다고 생각한다. 나중에 민섭이한테 과외좀 받아야지 ㅎㅎ

멘토링떄 받았던 피드백 중 잘 지켜지지 않았던 것 중 하나가 중복되는 규칙을 공통 hook으로 처리하는 것이다. 각자 페이지마다 필요한 hook들을 개별적으로 구현하다보니 어떤 것이 중복되는지 파악이 힘들었고, 결론적으로 각 component별로 hook을 구현하기로 결정하였다.

개발 과정

드디어 개발이 시작되었고, 기본적인 CRUD util부터 구현하고 있었는데 나를 가장 많이 괴롭혔던 것은 다름아닌 eslint였다. 기존의 개발 방식은 개발 언어의 문법만 맞춰 개발하였지만, 우리가 사용한 Airbnb의 eslint 규칙은 생각보다 까다로웠다. 웬만한 것들은 검색을 해가며 맞춰나갔지만 애래와 같이 너무 까다로운 규칙들은 결국 팀원들의 동의를 얻은 후 rule에서 제외시키기로 했다.

image

앗 그리고 여담으로 branch 전략대로 PR을 올리는 것이 익숙치 않아 가끔 main branch에 직접 PR을 올리고 merge할뻔한 대참사가 일어날 뻔 했지만, branch protection을 걸어두고 우리 똑똑한 팀원들 덕분에 금쪽이가 될뻔 했지만 무사히 main branch를 지켜낼 수 있었다 ㅎㅎ…

첫주차에는 “같이 먹을사람 구해요” 모달 작업을 했다. 기능구현은 생각보다 빨리 끝냈지만 css는 통일성을 위해라는 핑계를 대며 민섭이한테 맡겨버렸다. 정작 페어프로그래밍 동료였던 민섭이 작업할 때는 피드백도 한마디 못해주고 좋은 동료가 되어주지 못해서 너무 미안했다…

우리가 처음 목표했던 필수기능 구현이 생각보다 일찍 끝나서 2주차에 접어들어서는 각자 하고싶었던 추가기능을 구현해보기로 했다. 나는 모달창에 지도 API를 불러와서 추가하는 아이디어를 냈고 은지랑 페어프로그래밍을 진행해보기로 했다. 나 역시 처음 다뤄보는 지도 API였기때문에 쉽지 않을 것이라는 생각을 했고 역시나 쉽지 않았다…

Kakao Developers에 개발자 등록을 하고 예제 코드를 보았는데 죄다 JS 코드밖에 없었다. 열심히 구글링을 하고 GPT한테도 물어보았지만 레퍼런스가 많이 존재하지 않아 직접 TS 코드로 변환해야겠다는 결론을 내렸다.

일단 예제에 있는 JS코드를 붙여넣으니 200개가 넘는 에러가 발생했다. 한숨만 나왔지만, 우리 팀만의 추가기능을 구현하기 위해 하나하나 에러를 고쳤다. 지금 생각해보니 짜증을 많이 냈던것 같은데 옆에서 은지가 많이 달래줬던것 같다 ㅎㅎ…(은지야 고맙다 😂)

결국 컴파일 에러를 다 고치고 지도가 잘 뜨나? 했는데 어림도 없다. 런타임 에러가 시작되었다. 처음에는 API 키를 못읽는 것 같아 env 파일에 넣어두었던 API 키를 직접 코드에 넣는 초짜 개발자같은 짓을 했다… (혹시 레포를 뒤지다가 API 키를 발견하신 분들이 계시면 눈감아 주시기를…) 어찌저찌 런타임 에러는 잡아냈지만, 화면에는 아무것도 보이지 않았다. 코드를 뚫어지게 쳐다보고 “여긴가?” 해서 고쳐봐도 지도는 묵묵부답이었다.

저녁떄가 되어 모두들 배고파하는 상황이었지만 나는 이 이슈를 해결하고 마음 편하게 저녁을 먹고 싶었다. 심기일전하고 다시 레퍼런스의 코드를 한줄한줄 읽다가 문득 발견한 코드 한 줄이 있었다.

const script = document.createElement('script');
script.src = `//dapi.kakao.com/v2/maps/sdk.js?appkey=${process.env.REACT_APP_KAKAO_API_ID}&libraries=services&autoload=false`;
document.head.appendChild(script);
script.addEventListener('load', onLoadKakaoMap);

여기서 script element를 선언해주고 load시에 listener를 등록해주는 코드를 빠뜨렸던 것이다. 그래서 지도를 load해주는 onLoadKakaoMap 함수가 동작하지 않아 아무것도 렌더링되지 않았던 것이덨다. 이 코드 한 줄의 부재로 거의 1시간 넘게 씨름을 했다니 허무했지만, 한편으로는 하루만에 API 구현을 끝내고 무엇보다 저녁을 먹으러 갈 수 있어서 좋았다. 옆에서 끝까지 도와준 은지한테도 너무 고마웠다.(+ css 예쁘게 잡아줘서 고마워용 😊)

2차 멘토링

빠르게 일주일이 지나 2차 멘토링 시간이 다가왔다. 1차 멘토링때와는 달리 기술적인 질문을 많이 했다. 내가 했던 질문은 다음과 같았다.

외부 API를 사용할 때 타입을 설정하는 것에 어려움이 많았습니다. 외부 API나 서드파티 라이브러리를 사용할 때 해당 라이브러리나 API의 미리 만들어진 타입들을 파악하기 위한 방법론이 궁금합니다.

멘토님의 답변은 일단 공식문서를 참고하는 것이다. 공식 문서에 타입이 지정되어있지 않으면 함수의 dependency에 접근해보고, dependency가 존재하지 않는다면 module까지 뜯어보는 습관을 들이라고 하셨다. JS를 사용한 개발에서는 한변도 겪어보지 못했는데 TS로 개발하려니 타입 설정에 유독 어려움을 만힝 겪었다. 처음에는 막막해서 any타입으로 일단 지정을 해봤는데, 멘토님이 피드백 주신대로 조금만 찾아봐도(console.log로만 찍어봐도) 타입을 알 수 있었다. 결국, 뜯고 뜯어도 타입을 특정할 수 없는 몇몇 말썽꾸러기들을 제외하고는 다음과 같이 interface를 통해 타입을 지정해 주었다.

 interface Place {
    address_name: string;
    category_group_code: string;
    category_group_name: string;
    category_name: string;
    distance: string;
    id: string;
    phone: string;
    place_name: string;
    place_url: string;
    road_address_name: string;
    x: string;
    y: string;
  }

  interface PaginationData {
    current: number;
    first: number;
    gotoFirst: () => void;
    gotoLast: () => void;
    gotoPage: (pageNumber: number) => void;
    hasNextPage: boolean;
    hasPrevPage: boolean;
    last: number;
    nextPage: () => void;
    perPage: number;
    prevPage: () => void;
    totalCount: number;
  }

  const placesSearchCB = (
    data: Place[],
    status: string,
    pagination: PaginationData,
  ) => {

이어지는 개발 과정

생각보다 지도 API 구현이 빨리 끝나 어떤 추가기능을 넣을지 고민하던 중, pagnation 기능이 필요하다고 느껴 같이 할일이 끝난 은지랑 pagnation을 구현해보기로 했다. 사실 JS 과제를 하며 pagnation을 구현해보려고 했지만, 시간상 포기했던 경험이 있어 걱정이 앞섰다.

근데 왠걸… 은지가 옆에서 잠시 구글링하더니 react-js-pagnation 라이브러리를 소개해주었다. 사용 방법도 정말 간단했다.

const PaginationComponent: React.FC<PaginationProps> = ({
  activePage,
  totalItems,
  itemsPerPage,
  handlePageChange,
}) => {
  return (
    <div className="pagination">
      <ReactPaginate
        activePage={activePage}
        itemsCountPerPage={itemsPerPage}
        totalItemsCount={totalItems}
        pageRangeDisplayed={5}
        onChange={handlePageChange}
        itemClass="page-item"
        linkClass="page-link"
      />
    </div>
  );
};

다음과 같이 component를 만든 후, 다음과 같이 state를 하나 선언하여 기존 list에서 해당 page에 표시할 item들만을 렌더링해주면 끝이었다.

const [currentPage, setCurrentPage] = useState(1);

...

const itemsPerPage = 6;
const indexOfLastItem = currentPage * itemsPerPage;
const indexOfFirstItem = indexOfLastItem - itemsPerPage;
const currentItems = list.slice(indexOfFirstItem, indexOfLastItem);

const handlePageChange = (PageNumber: number) => {
  setCurrentPage(PageNumber);
};

라이브러리를 사용하니 불과 1시간만에 pagnation 기능을 구현하였다. (구현이라고 하기도 뭐하다…) 어쨌든 기능 구현을 하고 은지한테 css를 맡겼다. 늘 느끼지만 나는 css가 가장 어려운것 같다 ㅠㅠ 😒

추가로, 이전에 구현했던 지도 API의 목록 중 하나를 클릭하거나 표시된 마커를 클릭하면 그 장소의 위치로 지도가 focus되고, 모달창에 표시되는 위치 정보를 클릭한 장소로 설정하는 기능이 필요하다는 피드백이 있어 구현해보았다. 이 기능도 생각보다 쉬웠다.

function getListItem(position: any, index: any, places: any)
...
function addMarker(position: any, idx: any, place: any)

위의 두 함수의 parameter로 place라는 위치값을 넣어주었다. (이떄는 아직 type 지정을 해주지 않아 any type으로 선언해놨었다.)

el.addEventListener('click', () => {
  console.log(places.place_name);
  handlePlaceClick(places.place_name, places.address_name);
  map.setCenter(position);
  map.setLevel(5);
});

...

window.kakao.maps.event.addListener(marker, 'click', () => {
  handlePlaceClick(place.place_name, place.address_name);
  map.setCenter(position);
  map.setLevel(5);
});

이후, 받아온 parameter의 위치 정보를 바탕으로 모달창에 표시되는 위치 정보를 설정하고, 지도도 선택한 위치로 focus할 수 있게 listener를 달아주었다.

images

그 결과, 목록에서 식당을 클릭하거나 마커를 클릭 시, 위와 같이 맛집 이름과 맛집 위치가 지도에서 불러와져서 보여지며, 지도도 focus되는 모습을 볼 수 있었다.

시행착오

개발 마무리 무렵, 시연 영상을 준비하고 있었는데 갑자기 이미지가 로드가 안되는 현상이 발생했다. 나만 그런줄 알았는데 다른 팀원 모두가 동일한 현상을 목격했다. 깜짝 놀라 DB에 접속해보니 하루에 할당된 트래픽을 모두 소진해서 더이상 요청을 할 수 없었다.

이전에도 트래픽이 몰려 DB가 터지는 현상이 있었는데 그때는, 민섭이가 useEffect 내에서 DB listener를 달아줘서 발생한 문제였었다.

useEffect(() => {
  const unsubscribe = getDataBySnapshot('with-collection', setDatas);

  return () => {
    unsubscribe();
  };
}, []);

이를 해결하기 위해 useEffect 내에서 사용되던 listener를 반환하여 사용하였다.

해당 조치를 해주었음에도 또 DB의 트래픽이 몰리자, 팀원들은 해결 방안을 생각해보았다. 일차적으로, 갤러리에 올릴 수 있는 파일의 확장자 제한 및 파일 크기 제한이었다. 은지가 파일 확장자를 사진 파일로 제한하였고, 나는 다음과 같이 파일 크기를 지정해 일정 크기를 넘는 파일은 업로드를 제한시켰다.

if (file.size > MAX_IMAGE_SIZE_BYTES) {
  swal({
    icon: 'error',
    title: '이미지 크기 제한 초과',
    text: '2MB 이하의 이미지를 업로드해주세요.',
  });
  return;
}

하지만, 아무리 파일의 확장자와 크기를 제한한다 하더라도 다른 음식 카테고리를 누를 때, page가 변경될 떄 불필요한 요청이 계속해서 발생하고 트래픽이 빠르게 증가하는 근본적인 해결방법은 아니었다. 만약 리펙토링을 진행한다면, 이 부분을 해결하는 것이 급선무일 것 같다.

또한, 테스팅을 하면서 발견한 미스터리한 오류도 있었다. 원래 갤러리에 있는 각 요소들은 마우스를 hover시 “변경” 버튼이 나타나고, 해당 버튼을 클릭 시 변경 모달이 나타나야 한다. 하지만, 어떤 이유에서인지 특정 요소의 모달을 한번 열고 나면 다음과 같이 “변경” 버튼을 클릭하지 않고 요소가 렌더링된 영역에 마우스가 hover만 되어도 모달이 나타나는 현상이 발생했다.

image

이 에러를 잡기 위해 팀원들과 몇 시간동안 코드를 분석해도 도무지 에러를 찾을 수 없었다. 그래서 이 에러는 여전히 우리 프로젝트의 미스터리이자 옥에 티로 남겨져있다… (혹시 이유를 안다면 알려주길 바란다.)

마치며…

지금까지 주저리주저리 첫 토이프로젝트에 대한 회고록을 써보았다. 사실 이런 회고가 처음이라 앞뒤가 안맞고 논리정연하지 않을 수 있지만 감안해주시리라 믿는다 😊

어쨌든 이번 토이프로젝트는 혼자서 과제 수행만 하다가 처음으로 팀을 만들어 진행했던 프로젝트였는데, 팀원들 한명한명 다양한 방면에서 역량이 뛰어나서 모르고 있던 부분이 많음을 느꼈고 여러가지를 배울 수 있었던 시간이었다. 처음으로 오프라인으로 만나 사람들과 친해져보니 힘들지만 재밌었고 열심히 공부해서 다음 프로젝트에서는 좀 더 능력발휘를 해봐야겠다는 동기부여가 되었다.

카테고리:

업데이트: