시작하며…

벌써 파이널프로젝트가 끝나고 7개월간의 대장정이 마무리되었다. 어제 수료식을 마치고 마지막 퇴실 qr을 찍으니 진짜 과정이 끝난 것이 실감났다. 뭔가 시원하면서도 허무했다. 그리고 그것도 잠시, 내일 모레부터 출근이라는 사실이 믿기지 않았다. 시차적응도 해야하고 알고리즘, CS 공부도 해야하고 자격증도 따야되는데… 2월달은 이런것들을 하면서 내실을 쌓고 싶었지만, 인턴십 시작을 한 달 일찍 할 수 없냐는 부탁을 받았기 때문에 쉴 틈 없이 일을 시작하게 되었다. 하지만, 그렇다고 회고를 거를 수는 없기 때문에 출근 전날 급하게 카페에 달려와서 회고를 써본다.

팀빌딩 미팅

파이널 조가 발표되고 서로서로 누구와 되었는지, 주제가 무엇인지 확인하고 있었다. 내 주제는 1지망이었던 “여행 여정 공유 플랫폼” 대신 2지망이었던 “무료 예약 취소 불가한 숙소의 양도/거래” 플랫폼이었다. 조원을 처음에 확인했을 때, 아는 사람이 아무도 없어 걱정했지만 다들 한번씩 들어본 쟁쟁한 사람들이라 내심 좋았다. 나영이랑도 서로 어느 조 됐냐고 톡을 하다가 우리 조에 나영이랑 친했던 솔미가 있다고 엄청 좋아하며 친해지라고 했다.

걱정 반 설렘 반의 마음으로 첫 회의에 들어갔다. 어색한 분위기 속 회의가 진행되다가 파트별로 팀장을 뽑는 시간을 가지기로 했다. 처음에 재준이형이 나보고 팀장 잘할것같다고 하길래 식겁하면서 말을 급하게 돌렸다 ㅎㅎ 결국 간단한 아이스브레이킹 끝에 재준이형이 팀장이 되었다.

첫날에는 간단한 기술 스택과 컨벤션, 추후 논의사항 등을 정했다. 팀원마다 선호하는 컨벤션에 사소한 차이가 있었지만 큰 문제 없이 컨벤션 규칙을 확정지었다.

기획, 디자인

사실 이번 프로젝트의 공식적인 개발 기간은 12월 26일부터였는데, 그 전주부터 개발, 디자인 분들과의 회의가 진행되었다. 나는 이 사실을 모르고 미니프로젝트 KPT가 끝나자마자 도쿄로 떠나버렸다. 도쿄에서 제대로 회의 참석을 못하고 전해듣기만해서 너무 죄송할 따름이다 ㅠㅠ

내가 도쿄에 있을 때 일주일에 2~3번씩 기획 파트와의 회의가 진행되었다. 프로젝트의 방향성은 야놀자의 산하 프로젝트 느낌으로 가져가기로 하였고, 생각보다 기획이 너무 기초적인 기능들로 구성되어 있어서 FE끼리 나중에 추가하면 좋을만한 내용들을 구상해보았다.

기획이 어느정도 진행되자, 기획 내용 중 기술적으로 구현 가능한 기능들인지 FE 내부적으로 회의하는 시간을 가졌다.

image

위와 같이 기능별로 다양한 레퍼런스를 찾아보고 기획과 BE 분들에게 구현 가능할지 불가능할지 전달해드렸다. 추가로, 역할도 분배했는데 나는 마이페이지 + 거래 페이지를 맡게 되었다. 다행히 결제 관련된 작업을 한 번 해보고싶었는데 역할 분배가 만족스럽게 되었다 ㅎㅎ

마지막으로, 우리 필요젝트에 꼭 필요한 기능들에 대해서 상의해보았다. 가령 찜하기나 리뷰같은 기본적인 기능이 기획안에 없어서 기획분들께 전달드렸다.

개발 시작(?)

크리스마스가 지나고, 12월 26일이 되자 우리는 레포지토리를 생성하고 초기 세팅을 진행했다. 기본적인 패키지 설치, Eslint와 Prettier 설정 등을 진행했다. 추가로, PWA 서비스워커를 등록하여 모바일 환경에서 앱처럼 다운받아 사용할 수 있도록 하였다.

image

다음 회의에서는 위와 같이 페이지별 세부 기능들에 대해 궁금한 점들을 기획 파트와 공유하여 답변들을 정리하였다.

12월 말이 되자, 어느 정도 와이어프레임이 구체화 되어서 중간 완성 목표(FE의 목표는 마크업)일과 세부적인 역할 재분배를 진행하였다. 개인적으로, 이때부터 개발이 바로 진행되었어야한다고 생각했는데 이후에도 기획과 개발단 사이에 지도 필터링, 키워드 알림, 초성 검색 등 기술적으로 논의가 더 필요한 내용들을 이야기해야했고, 이밖에도 취소 수수료, 숙박 데이터 등을 어떻게 설정할지에 대한 논의도 필요했기에 일주일정도 개발이 더 밀렸다.

본격적인 개발

1월 둘쨰주가 되어서야 본격적인 개발이 시작되었다. 기존 개발과 다른 점이 있었다면, 디자이너분이 여러 군데 재사용되는 공통 컴포넌트를 지정해주셔서 공통 컴포넌트를 먼저 만들기로 했다. 역할을 나누어 공통 컴포넌트를 만들기로 했는데, 나는 버튼을 맡았다. 어렵지 않은 내용이었지만, 다른 사람들이 사용하기 쉽게 만들려고 노력하다보니 생각보다 공수가 많이 들었다. 무엇보다, 버튼 컴포넌트의 양이 다양한 것도 한몫했다.

image

멘토링

공통 컴포넌트를 만들며 발생한 이슈에 대해서 멘토링 시간을 이용해 멘토님께 질문을 드렸었다.

interface CardSectionProps
  extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "type"> {
  type: "abledPay" | "disabledPay" | "abledPoint" | "disabledPoint";
  width?: string;
}

위처럼 CardSectionPropsOmit을 이용해서 type 속성을 재정의 한 후, <S.BottomLeftButton type={type}> 이런 식으로 styled-component 로 type 속성을 내려주어서

export const BottomLeftButton = styled.button<CardSectionButtonProps>`
  ${ButtonLayout}
  background: ${({ theme, type }) =>
    type === "abled" ? theme.colors.orange[100] : theme.colors.gray[100]};
`;

styles 파일에서 구조분해할당을 통해 theme와 type을 가져와 type에 따라 색상을 달리하는 로직을 구성하였다.

하지만 기대와 달리,

This JSX tag's 'children' prop expects type 'never' which requires multiple children, but only a single child was provided.
'S.BottomLeftButton' cannot be used as a JSX component.
Its type 'StyledComponent<{ theme?: Theme | undefined; as?: ElementType<any, keyof IntrinsicElements> | undefined; } & CardSectionButtonProps, DetailedHTMLProps<...>, {}>' is not a valid JSX element type.
Type 'StyledComponent<{ theme?: Theme | undefined; as?: ElementType<any, keyof IntrinsicElements> | undefined; } & CardSectionButtonProps, DetailedHTMLProps<...>, {}>' is not assignable to type '(props: any, deprecatedLegacyContext?: any) => ReactNode'.
Types of parameters 'props' and 'props' are incompatible.
Type 'any' is not assignable to type 'never'.
The intersection '{ theme?: Theme | undefined; as?: ElementType<any, keyof IntrinsicElements> | undefined; } & CardSectionButtonProps & ClassAttributes<...> & ButtonHTMLAttributes<...>' was reduced to 'never' because property 'type' has conflicting types in some constituents.

이러한 에러가 발생하였다. 확인 결과, theme의 속성이 never로 지정되어있었고, 여러 방법을 시도해보다가 CardSectionProps를 다음과 같이 재정의하였더니 문제가 해결되었다.

interface CardSectionProps {
  types: "abledPay" | "disabledPay" | "abledPoint" | "disabledPoint";
  width?: string;
}

관련해서 type 속성을 재정의하는 것이 안되는건지, 아니면 다른 원인이 있었던 것인지 모르겠어서 질문을 드렸다.

멘토님의 답변은 다음과 같았다.

💡 typescript에서 type 정의를 바꾼다고 기본 attribute 동작이 바뀌지 않는다는 점 입니다. react에서 className이라는 명칭으로 기본 속성인 class 라는 기본 스펙을 피해가는 것도 같은 이유가 됩니다. 하단에 에러는 아마 type에 대한 타입충돌도 있을 것 같습니다. typescript는 사용 설명서 입니다. 예를 들면 카메라에 대한 설명서를 바꾼다고 카메라가 다른 동작을 하지 않는 것과 같은 부분입니다. 다른 속성 이름을 정해서 컨트롤 하는 것이 일반적이고 안전합니다.

내가 감히 기본 속성인 type 속성을 재정의하려했던 것이었다ㅠㅠ 그래서, 멘토님 말씀대로 다른 속성 이름을 생각하여 에러를 해결했다.

이어서…

공통 컴포넌트를 다 만들고 나니 어느새 마크업을 마치기로 했던 중간 점검일이 얼마 남지 않았다. 서둘러서 마크업을 진행하였지만, 중간점검때까지 2개 페이지밖에 마크업을 해내지 못했다.

구영표 멘토님 특강

다음주 월요일이 되었고, 구영표 멘토님의 클린코드 특강이 있는 날이었다. 마침 재준이형도 일주일간 서울에 올라와있다고 해서 특강도 들을 겸 오프라인으로 만나기로 했다. 7시까지 작업을 하다가 저녁을 먹으러 가기로 했다. 원래는 우리조끼리 먹으려다가 강의장에 있던 사람들이 하나 둘씩 모여 10명이 되었다. 같이 맥주도 한잔 하면서 자연스럽게 친해지고 말도 놓았다. 다들 엄청 친화력 좋고 착한 사람들이라 너무 재밌었던 자리였다.

FCM 연동 에러핸들링

알림 기능을 구현하기 위해 FCM(Firebase Cloud Messaging)을 연동해야 했었다. 이 기능은 역할 분담이 되어있지 않아서 누가 할지 회의를 하다가 나랑 솔미가 맡게 되었다. 처음 사용하는 기능이었지만 레퍼런스도 꽤 있었고 생각보다 간단해 보여서 금방 끝내고 마크업을 이어갈 수 있을 것이라 생각했다.

하지만, 솔미랑 밤을 새가며 FCM 연동을 위해 노력했지만 연동은 커녕 serviceWorker 등록도 어려웠다.

마주쳤던 에러는 크게 3개였다.

1.

The package "@esbuild/linux-x64" could not be found, and is needed by esbuild.

이 에러는 미니프로젝트떄도 마주한 에러였는데, 여러 해결책을 찾아보다가 다음과 같이 optional dependencies에 패키지를 설치하여 해결하였다.

"optionalDependencies": {
  "@rollup/rollup-linux-x64-gnu": "4.6.1",
  "esbuild": "^0.19.11"
}

2.

An error occurred while retrieving token. FirebaseError: Messaging: We are unable to register the default service worker

이 에러때문에 계속 고생했는데 원인은 firebase-messaging-sw.js 파일을 root 디렉토리에 위치시켰기 때문이었다. public 디렉토리로 해당 파일을 이동하여 해결하였다.

3.

importScripts firebase not working

이 에러도 상당히 애를 먹였는데, 결론적으로 경로에 -compat 추가하고, self.importScripts가 아닌 importScripts로 변경하고, 이때 발생하는 eslint 에러에 대해서 /* eslint-disable */ 로 disable 처리를 하여 해결하였다.

멘토링

위 에러를 해결하면서 일반적인 import 방식이 아닌 importScripts를 사용하여 cdn처럼 import를 해주었는데 왜 이걸 사용하는지 궁금해져서 멘토링 시간을 이용하여서 질문을 하였다.

멘토님의 답변은

하나 이상의 스크립트를 웹워커에 등록해 쓰레드를 만들어주며 동기적으로 가져오기 위함.

이었다.

참고

  • 웹 워커란?
    • 웹페이지에서 스크립트가 메인 스레드에서 실행되는 동안, 웹워커는 백그라운드에서 별도의 스레드에서 동작하는 자바스크립트 스크립트
    • 주로 CPU 집약적인 작업이나 긴 시간이 걸리는 작업을 메인 스레드와 별개로 처리하여 웹페이지의 성능을 향상시키는 데 사용됨

추가로, 서비스 워커의 개념에 대해서도 질문드렸는데, 간단히 말하자면 “싱글 쓰레드인 js 에서 성능적으로 main 쓰레드에 영향을 주는 처리를 다른 쓰레드에서 처리해서 결과만 이벤트를 통하여 완료된 시점에 받는 방법”이라고 하셨다.

만만하게 봤다가 생각보다 엄청 고생을 했지만 그만큼 얻어가는 것도 있었던 FCM과의 사투 내용이었다… (솔미야 너도 그렇게 생각하지? ㅋㅋ)

다시 한 번 밤을 새가면서 고생한 솔미와 다른 팀인데도 서로 에러 핸들링을 도와준 나영이에게 고맙다는 말을 하고싶다.

API 연동

결국, 앞서서 말한 이런저런 이슈 때문에 1월 셋째주가 시작되고 나서야 마크업을 완료했지만, 이후에도 변경사항 반영, 예외케이스 디자인, 유효성 검사 로직을 추가하다가 일주일이 또 지나가버렸다.

결국 최종발표 일주일만을 앞두고 API 연동을 들어갔는데, 이때까지만 해도 모든 API가 배포되지 않아서 MSW를 이용하여 목업을 만들어 연동을 해놨다. 이때, 밤을 새가면서 1pr 1500줄 addition이라는 미친 작업을 했다.

image

하지만, 이때 API 최종 배포가 얼마 남지 않은 상황에서 MSW로 API를 연동시켜놓은것은 큰 실수였다…

불과 다음날 API가 최종 배포되었고, 하루만에 MSW 로직 대신 실제 API로 로직을 교체해야만 했다. 나는 instance의 주소정도만 바꾸면 잘 되겠지~ 했지만 생각보다 닉네임 변경 로직, 핸드폰 번호 인증 로직 등 손봐야하는 경우가 많았고 목업을 이용했을 때 미처 생각하지 못했던 무한스크롤 로직 등 추가해야할 사항이 많았다.

추가로, 판매/구매내역 페이지에서 쓰이는 ListCard 컴포넌트의 종류가 워낙 다양해서 곯머리를 앓았다.

image

게다가, API에서 response로 주는 상태는 내가 정의한 cardType과 달라 이를 분기하여 처리하는 것이 매우 복잡했다.

어찌저찌 마이페이지 API 연동이 끝나고 결제 페이지는 바로 실제 API와 연동을 시켰다.

결제 페이지는 무난하게 연동이 진행되나 했지만 역시나 여러 에러가 나를 기다리고 있었다. 상품 상세 보기에서 결제하기 버튼을 눌러 navigate를 시켰을 때는 아래와 같이 data가 정상적으로 호출되었지만,

image

해당 페이지에서 새로고침을 하면 아래와 같이 query들이 모두 inactive한 상태로 바뀌어 데이터가 호출되지 않았다.

image

관련해서 재준이형한테도 SOS를 요청했지만 해결되지 않아 멘토링 시간에 멘토님과 같이 해결해 보았다.

멘토링

멘토님의 솔루션은 쿼리를 fresh한 상태로 유지해보거나 mount시 refetch하는 옵션을 주어보라고 하셨다. 하지만 두 방법 모두 먹히지 않아 고민하다가 data가 undefined 일때도 일단 화면에 출력되게 하니 해결되었다. 나중에 알고보니 data의 type이 명시적으로 지정되지 않아 발생한 에러였다. typescript를 사용하면서 명시적 타입 선언이 얼마나 중요한 부분인지 느낀 부분이었다.

추가로, 미니프로젝트 때와 유사한 문제였는데, 자식 컴포넌트로 refetch 함수를 내려주고 refetch를 자식 컴포넌트에서 trigger 시켜주면 부모 컴포넌트의 데이터가 refetching 되는것을 기대했는데, 바뀌지 않아 reload()로 새로고침을 해주었었다.

아무리 생각해도 이 방법이 최선이 아닌것같아서 멘토님과 원인을 찾다가 “비동기 함수인 mutate 함수의 실행이 끝나기 전에 refetch를 시켜주어 기존 stale한 데이터가 refetching 되는 것 아닌가?” 라는 생각이 들어 mutate 이후, isSuccess일 때만 refetch를 trigger해주었더니 해결되었다.

이 에러핸들링을 하면서 비동기함수의 동작 이후에 trigger되는 로직들에 대한 처리를 어떻게 해야할지 많이 공부할 수 있었다.

QA

원래 24일부터 QA를 이틀간 진행하고 26부터 이틀간 UT(User Test)를 진행하려고 했지만, 생각보가 개발 일정이 밀리면서 기획 분들께 죄송하게도 UT는 지인을 통해 간단하게만 진행하고 QA에 집중하기로 했다.

QA가 시작됐을 때만 해도 FE 자체적으로 테스트를 해보았을 때도 매우 불안정한 느낌이 들었다. 기획분들께도 아직 많은 기능이 안될것이라고 말씀드리는데 너무 죄송했다 ㅠㅠ 하지만, 기획분들이 “이거 왜 아직 안돼요?” 하지 않고 이슈들에 대해서 다음과 같이 문서로 정리를 해주신 덕분에 찬찬히 하나하나 고쳐나갈 수 있었다.

image

내 파트의 QA 내용은 많지 않았지만, 그럼에도 불구하고 9차 QA까지 진행할 만큼 꼼꼼히 QA를 진행하였다.

image

가장 기억에 남았던 내용은 모바일 토스페이 결제 시 페이지가 자동으로 redirect되어 결제 요청이 되지 않는 오류였다.

useEffect(() => {
  if (isPaymentSuccess) {
    buyProductMutate(mutateInfo);
  }
}, [isPaymentSuccess]);

useEffect(() => {
  if (buyProductSuccess) {
    navigate(`/purchase/confirm?${purchaseInfoQueryString}`);
  }
}, [buyProductSuccess]);

pc에서는 결제 이후, setIsPaymentSuccess를 통해 isPaymentSuccess값이 변경되고, 이후, mutate 함수를 호출한다. mutate가 성공하면, 관련된 결제 정보를 queryString에 담에 결제 완료 페이지로 navigate하는 로직이라 큰 이슈가 없었다.

하지만, 모바일 결제 시 결제가 끝나면,

{
  pg: "tosspayments",
  pay_method: "card",
  name: accommodationName, // 상품명
  amount: amount,
  buyer_name: name,
  buyer_tel: phoneNumber,
  m_redirect_url: redirectUrl // 모바일에서 결제시, 페이지 주소가 바뀜, 따라서 결제 끝나고 돌아갈 주소 입력해야됨
},

params로 받아온 redirect url로 강제로 이동시켜버려서, 결제 성공 시 동작하는 callback 함수가 실행되지 않았다. “그럼 어떻게 결제 완료 페이지에서 결제 정보를 받아오고 결제 요청은 어떻게 보낼까?” 라는 이슈가 발생하였다.

처음에는 결제 프로세스를 trigger하는 함수를 호출하기 전에 localStorage에 결제 정보를 set해주었다. 하지만, 모바일 결제 시에는 결제 중 도메인이 변경되어서 결제 정보가 몽땅 날아가버렸다. 다른 도메인의 스토리지에는 접근이 불가능하기 때문이었다. indexDB를 이용해보았지만, 이 역시 마찬가지로 도메인 변경 시 스토리지에 접근이 불가능했다.

결국, 이 경우 queryString을 이용하여 data를 전달해야한다고 판단, 결제 정보를 queryString에 담아서 props로 REDIRECT_URL을 직접 내려주기로 하였다.

const purchaseInfo = {
  accommodationName: productData?.accommodationInfo.name,
  roomName: productData?.roomInfo.name,
  checkInDate: productData?.checkInDate,
  checkOutDate: productData?.checkOutDate,
  checkInTime: productData?.roomInfo.checkInTime,
  checkOutTime: productData?.roomInfo.checkOutTime,
  minHeadCount: productData?.roomInfo.minHeadCount,
  maxHeadCount: productData?.roomInfo.maxHeadCount,
  reservationPersonName: nameState,
  reservationPersonPhoneNumber: phoneNumberState,
  userPersonName: name,
  userPersonPhoneNumber: phoneNumber,
  tradeId: productData?.tradeId,
  productPrice: formatNumberWithCommas(productData?.price),
  fee: (productData?.sellingPrice * 0.05).toString(),
  point: pointToUse,
  totalPrice: formatNumberWithCommas(totalPrice),
  paymentType: paymentMethod,
  productId: productId,
  image: productData?.accommodationInfo.image,
};

const purchaseInfoQueryString = new URLSearchParams(purchaseInfo).toString();

const REDIRECT_URL = `https://www.yanabada.com/purchase/confirm?${purchaseInfoQueryString}`;

...

onClick={() => {
  if (paymentMethod === "tossPay") {
    onClickTossPayment(
      productData?.accommodationInfo.name,
      nameState,
      phoneNumberState,
      totalPrice,
      setIsPaymentDone,
      REDIRECT_URL
    );

...

엄청난 양의 결제 정보가 있기에 어떻게 queryString을 일일히 설정할지 고민이었지만, 다행히 URLSearchParams를 이용하여 한번에 queryString을 설정해주었다.

추가로, 모바일 결제 시, REDIRECT_URL에 isMobile=true 속성을 하나 더 추가해주었다.

m_redirect_url: `${redirectUrl}&isMobile=true`;

이후, 결제완료 페이지에서는 아래와 같이 useLocationquery-string라이브러리를 이용하여 queryString으로 넘어온 정보를 parsing하여 사용하였다.

const location = useLocation();

const purchaseInfo = queryString.parse(location.search);

하지만, 결제 정보를 잘 받아온 이후에도 “결제 요청은 언제 보낼까?”라는 한 가지 이슈가 남아 있었다. queryString을 이용하여 결제 정보는 받아왔지만, 결제 요청을 보내는 mutate 함수를 아직 호출하지 않은 상황이었다. 결제 프로세스를 trigger하는 함수를 호출하기 전에 mutate 함수를 호출하기에는 결제 프로세스가 아직 끝나지 않았는데, 서버에 결제 요청을 보낼 수 없었다.

결국 우회 방법으로 생각해 낸 것이 “결제 완료 페이지에서 mutate 함수를 호출하자” 였다. 단, 결제 정보가 존재할 경우에만 말이다.

useEffect(() => {
  if (isMobile === "true") {
    buyProductMutate({
      productId: Number(purchaseInfo?.productId),
      reservationPersonName: purchaseInfo?.reservationPersonName as string,
      reservationPersonPhoneNumber: purchaseInfo?.reservationPersonPhoneNumber as string,
      userPersonName: purchaseInfo?.userPersonName as string,
      userPersonPhoneNumber: purchaseInfo?.userPersonPhoneNumber as string,
      point: Number(purchaseInfo.point),
      paymentType: convertString(purchaseInfo.paymentType as string)
    });

이를 위해 결제 완료 페이지에서 parse한 purchaseInfo 객체의 isMobile값이 true이면, mutate 함수를 호출했다. 이때, mutate를 위해 필요한 data는 purchaseInfo 객체에서 가져왔다.

또한, mutate가 동작 완료되기 전까지는 거래 정보를 가져오면 그 데이터가 존재하지 않기 때문에 다음과 같은 로직을 추가하였다.

const usePurchaseHistory2 = () => {
  return useQuery({
    queryKey: ["purchaseHistory2"],
    queryFn: () => getPurchaseHistory2(),
    enabled: false,
  });
};

일단 위와 같이 purchaseHistory2 라는 queryKey를 가진 data를 disable 시켜주었다. 이후, 모바일 결제인 경우 mutate가 성공했을 경우에 거래 정보 data를 refetch 시켜주었다.

useEffect(() => {
  if (buyProductSuccess) {
    refetch();
  }
}, [buyProductSuccess]);

반면, pc 결제의 경우 mutate과정이 없기에 useEffect를 이용하여 mutate를 호출해주었다.

이렇게 꼼수 아닌 꼼수(?)를 부려서 모바일 결제 시에도 결제 요청이 이루어지게 로직을 수정하였다. QA 막판에 급하게 작업하느라 애를 먹었지만 나름 나쁘지 않았던 꼼수라고 생각한다 ㅎㅎ

마무리

이렇게 길다면 길고 짧다면 짧은 파이널프로젝트가 마무리되었다. 생각보다 논의할게 많아 개발이 지연되었고, 새로운 기능을 추가하기보다는 페이지가 많아 기획된 기능들을 구현하는데 급급했다. 테스트 코드도 작성하지 못한 채 QA를 진행하기 바빴지만, 짧은 QA 시간동안 완성도 측면에서 엄청난 발전이 있었다고 생각한다.

개발이 끝나고도 발표 준비와 영상 촬영에 힘써주신 덕분에 파이널 프로젝트 최우수 조로 선정될 수 있었다. 노력한 만큼 보상받은 기분이었다. 특히, 호진님이 야나바다 실제 서비스 출시하는거 아니냐고 하셨을때 진짜 기분 좋았다 ㅋㅋ 앞으로 더 이 프로젝트에 애정을 가지고 바빠도 조금씩이라도 리팩토링 해서 실제 서비스 못지않은 서비스를 만들어보고싶다.

마지막으로 파이널 프로젝트 기간동안 고생한 5조의 마법사 팀원 모두에게 다시 한 번 고생했다고 말하고 싶고 특히, FE 팀원들!!! 재준이형, 솔미, 예인누나, 수빈이 너무x3 고생했어.👍😊💕

image

패스트캠퍼스 X 야놀자 프론트엔드 개발 부트캠프 - 완 -

카테고리:

업데이트: