- Published on
텀블벅에서의 첫 프로젝트(광고 플랫폼) 회고록
- Authors
- Name
- Na Hyunwoo
회고록은 처음이라… gpt에게 물어보았습니다. “회고록은 어떻게 쓰는거야 ??”
이를 바탕으로 회고록의 목차를 짜봤습니다. (고마워 pt야..!)
프로젝트 개요
- 텀블벅은 크라우드펀딩 플랫폼으로, 창작자들이 자신의 아이디어나 작품을 소개하고 후원자 들로부터 자금을 모으는 서비스입니다.
- 창작자들은 광고 센터를 통해 자신의 프로젝트를 더 많은 사람들에게 노출시켜 더 많은 후원자를 모을 수 있도록 해줍니다.
- 이번에 제가 만든 광고 플랫폼 프로젝트는 광고를 등록하고, 광고 관리하기 수월하게 해주는 플랫폼 이었습니다.
어려웠던 점 + 해결 방안
[환경적 어려웠던 점]
- 아무래도 입사한지 얼마 안 되었던 상황이었기 때문에, 달라진 구성원들과, 달라진 개발환경에 대한 적응이 필요하다고 생각했습니다. 추가적으로 프로젝트가 이전부터 진행되고 있었기 때문에, 이전의 코드들에 대한 파악이 필요했고, 무엇보다 프로젝트에 참여했던 분들이 전부 퇴사한 상태였다는 점이 가장 큰 어려운 점이었습니다.
- 그러나 동시에 프로젝트의 마감 시한이 1달 남짓 남아있었기 때문에, 당장 개발에 돌입해야하는 상황이었습니다. 그래서 입사한지 3일정도 되는날부터 바로 개발에 돌입하였습니다.
[개발시 어려웠던 점]
광고 플랫폼을 단시간에 처리하면서 마주한 수많은 어려움들이 있었지만, 그 중에서 딱 3가지만 뽑으라면 아래와 같습니다.
주문 상세 내역 다양한 유즈 케이스
다음과 같이 하나의 컨테이너에서 프로젝트의 광고, 결제 두 가지의 상태에 따라서 엄청나게 많은 분기들이 발생하고 있습니다. 제가 이 부분에서 어려움을 느꼈던 부분은, 이 진행 상태와 관련된 컴포넌트를 하나의 컴포넌트를 재사용해서 만들려 했던 점입니다. 굉장히 다양한 케이스가 있지만, 결국 대부분의 UI가 비슷하기 때문에, 그렇게 짜는게 옳지 않나? 라는 생각을 했었습니다. 그러나, 위의 로직상 그렇게 짜는 것이 불가능하다는 판단이 있었고, 더불어 하나의 컴포넌트로 만들게 되었을 때, 컴포넌트의 복잡도가 급격하게 상승해 오히려 코드가 읽기 어려워진다고 판단하였습니다. 결국은,
컴포넌트의 재사용성을 포기하고, 분기에 따른 컴포넌트를 분리함을 통해 해결하였습니다. 위의 컴포넌트인 진행 상태 즉, OrderStatus는 다음과 같습니다.
export default function OrderStatus({ isNeedApproval }: Props) {
return isNeedApproval ? <OrderStatusNeedApproval /> : <OrderStatusNotNeedApproval />
}
위와 같은 분기 처리를 통해, 컴포넌트의 복잡성을 낮출 수 있었고, 정확하게 컴포넌트를 구현할 수 있었습니다.
광고 상품 선택에 따른 6가지 다른 컴포넌트
텀블벅 광고는 아래 그림과 같은 6개의 광고 상품이 존재합니다. 광고 플랫폼에서는 아래의 6가지의 광고 상품에 따라, 광고의 설정이 6가지로 나눠집니다.
광고 설정은 아래와 같이 기본적으로 캠페인명, 노출 기간, 노출 구좌를 가지고 있습니다.
위에서 선택된 6가지의 광고 상품에 따라 각각의 다른 캠페인명, 노출 기간, 노출 구좌를 보여줘야 했습니다. 추가적으로, 홈 배너 패키지의 경우 노출 구좌의 형태가 위의 사진과는 조금 다릅니다.
홈 배너 패키지 상품의 경우 노출 구좌는 아래 사진과 같습니다.
이와 같이 비슷한 듯 다른 6개의 광고 설정들을 처리하기 위해서 여러가지 방법에 대해서 고민을 한 끝에
하나의 컴포넌트를 다시 세부 컴포넌트로 쪼개기
라는 결론에 도달하였습니다. 더 풀어서 설명하면, 쪼개진 세부 컴포넌트 안에서 분기 처리를 하자 였습니다. 그렇게 생각하니 복잡도가 확 낮아졌습니다.
따라서, 광고 설정 컴포넌트를 다음과 같이 분리하였습니다.
- 캠페인 명
- 노출 기간
- 노출 구좌
으로 나누었습니다.
위와 같이 나눈 광고 설정 컴포넌트는 다음과 같습니다.
export default function AdsSetting() {
return (
<div>
<CampaignName />
<ExposurePeriod />
<ExposureAds />
</div>
)
}
그리고, 6개의 광고 상품에 따른 분기 처리를 나눠진 컴포넌트 안에서 처리를 했습니다. 각각의 컴포넌트 내부 모습은 다음과 같습니다.
export default function CampaignName() {
return (
<div>
<Title />
<Input />
</div>
)
}
export default function ExposurePeriod() {
return (
<div>
<ExposurePeriodGuide />
{isNeedConfirmMessage && <ConfirmMessage />}
</div>
)
}
export default function ExposureAds() {
return (
<div>
<ExposureAdsList />
{adType === 'HOME_BANNER' && <HomeBannerFormContainer />}
</div>
)
}
그리고 위와 같은 다양한 분기를 처리할 때 굉장히 유용하게 사용할 수 있는 타입스크립트의 유틸리티 타입이 있습니다. 그것은 바로, Record라는 녀석입니다. 이것은 다음과 같이 사용할 수 있습니다.
export const adsProduct: Record<TAdsProduct, TAdsType[]> = {
HOME_BANNER_PACKAGE: ['HOME_BANNER', 'CATEGORY_LIST', 'BOTTOM_OF_PROJECT_DETAIL'],
NOTABLE_PROJECT_PACKAGE: ['NOTABLE_PROJECT', 'CATEGORY_LIST', 'BOTTOM_OF_PROJECT_DETAIL'],
HOME_LIST: ['HOME_LIST'],
PROJECT_DETAIL_LIST: ['BOTTOM_OF_PROJECT_DETAIL'],
CATEGORY_LIST: ['CATEGORY_LIST'],
PRELAUNCH_LIST: ['PRELAUNCH_LIST'],
}
// example
const exposureAds = adsProduct['HOME_BANNER_PACKAGE']
// 결과: ['HOME_BANNER', 'CATEGORY_LIST', 'BOTTOM_OF_PROJECT_DETAIL']
- 캘린더 선택 로직
광고 플랫폼에서는 아래 영상에서와 같이 일반적인 캘린더와는 다르게 동작하고 있습니다.
따라서, 광고 플랫폼의 자체적인 캘린더를 만들어야 했습니다. 앞에서 말했듯이, 프로젝트를 중간에 이어받아서 하게 되었고, 캘린더는 UI가 완성되어있는 상태였습니다.
따라서, 저는 캘린더의 hover 동작과, 선택됐을 때의 동작, 그리고 취소, 초기화, 선택 완료 동작에 대해서 완성해야 했습니다.
위 캘린더 동작을 보면 하나의 날짜가 hover 되었을 때, [월, 화], [수, 목], [금, 토, 일]이 묶이고 있습니다. 그리고 하나의 날이 선택됐을 때도 똑같이 묶이고 있고, 선택된 날짜를 기준으로 선택 가능한 날짜를 UI를 통해 보여주고 있습니다.
이를 해결하기 위해 가장 단순하게 처리하기로 했습니다. 결국 요일이라는 것은 [월, 화, 수, 목, 금, 토, 일] 중 하나이기 때문에 switch 문을 통해 모든 분기를 처리하면 해결이 되겠다고 판단하였습니다.
현재 캘린더의 date는 “2023-03-11” 와 같이 “YYYY-MM-DD” 형태의 string으로 이루어져 있는데요, 이를 통해 요일을 판단하기 위해서는
export const getDayOfWeek = (date: string): string => {
const week = ['SUNDAY', 'MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY']
const dayOfWeek = week[new Date(date).getDay()]
return dayOfWeek
}
다음과 같은 형태의 함수를 만들어 판단해 주었습니다.
그리고 요일별로의 동작이 다른데, 예를 들어서, 월요일이 호버 되거나 선택 되었을 때에는 옆에있는 화요일이 같이 호버되거나 선택되어야 합니다. 반대로 화요일이 hover되거나 선택되었을 때에는 옆에있는 월요일이 호버되거나 선택되어야 합니다. 이를 해결하기 위해 다음과 같은 함수를 만들어서 해결하였습니다.
export const getCalcDate = (date: string) => {
const yesterday = subtractDate(date, 1)
const today = date
const tomorrow = addDate(date, 1)
return {
yesterday,
today,
tomorrow,
}
}
마지막으로 요일에 대한 판단을 하기 이전에, 하나의 분기를 더 처리해줬는데요, 그것은 그냥 date를 선택한 것인지 아니면 선택된 날 옆에 있는 선택 가능한 영역이 선택되었는지 판단하는 것이였습니다.
이렇게 분기처리를 통해 위와 같은 문제를 간단하게 해결할 수 있었습니다.
코드는 다음과 같습니다.
const handleSelect = (selectedDate: string) => {
if (selectedDates.includes(selectedDate)) {
return
}
if (continuousDates.includes(selectedDate)) {
handleContinuousDateSelect(selectedDate)
} else {
handleEnableDateSelect(selectedDate)
}
}
const handleContinuousDateSelect = (selectedDate: string) => {
...
switch (dayOfWeek) {
case 'MONDAY': {
if (tempSelectedDates[0] === selectedDate) {
setContinuousDates((prev) => [threeDaysAgo, dayBeforeYesterday, yesterday, ...prev]);
} else {
setContinuousDates((prev) => [...prev, dayAfterTommorrow, threeDaysAfter]);
}
break;
}
...
}
}
const handleEnableDateSelect = (selectedDate: string) => {
...
switch (dayOfWeek) {
case 'MONDAY':
setSelectedDates([today, tomorrow]);
setContinuousDates([
threeDaysAgo,
dayBeforeYesterday,
yesterday,
dayAfterTommorrow,
threeDaysAfter,
]);
break;
}
...
}
마지막으로, 캘린더는 compounds component pattern을 사용했기 때문에 다음과 같습니다.
<Calendar
from={availableDates[0]}
to={availableDates[availableDates.length - 1]}
displayedDate={displayedDate}
fundingStart={formattingFundingStartDate}
selectedDates={selectedDates}
continuousDates={continuousDates}
disabledDates={unavailableDates}
pairDatesOfHoverDate={pairDatesOfHoverDate}
setDisplayedDate={setDisplayedDate}
onSelect={handleSelect}
onMouseOver={handleMouseOver}
onMouseOut={handleMouseOut}
>
<Calendar.Header />
<Calendar.Contents />
<Calendar.Footer
isDisabled={selectedDates.length <= 0}
onClickCancel={handleClickCancel}
onClickComplete={handleClickComplete}
onClickReset={handleClickReset}
/>
</Calendar>
결과
- 프로젝트 구조
📦src
┣ 📂agreement
┃ ┣ 📂components
┃ ┗ 📂hooks
┣ 📂creator
┃ ┣ 📂components
┃ ┃ ┣ 📂AdsProduct
┃ ┃ ┣ 📂AdsSetting
┃ ┃ ┣ 📂AdsTargetProject
┃ ┃ ┣ 📂OrderInfo
┃ ┃ ┣ 📜ContentsBoxWithSubtitle.tsx
┃ ┃ ┣ 📜CreatorPageTitle.tsx
┃ ┃ ┗ 📜index.ts
┃ ┣ 📂hooks
┃ ┣ 📂types
┃ ┗ 📂utils
┣ 📂manager
┃ ┣ 📂components
┃ ┃ ┣ 📂AdsTableContents
┃ ┃ ┣ 📂AdsTableHeader
┃ ┃ ┣ 📂Banner
┃ ┃ ┣ 📂ManagerPageTitle
┃ ┃ ┣ 📜Layout.tsx
┃ ┃ ┣ 📜constants.ts
┃ ┃ ┗ 📜index.ts
┃ ┣ 📂order-detail
┃ ┃ ┣ 📂AdsCancel
┃ ┃ ┣ 📂AdsProduct
┃ ┃ ┣ 📂AdsSetting
┃ ┃ ┣ 📂AdsTargetProject
┃ ┃ ┣ 📂CampaignId
┃ ┃ ┣ 📂CreatorInfo
┃ ┃ ┣ 📂PaymentInfo
┃ ┃ ┣ 📂StickyArea
┃ ┃ ┣ 📜constants.ts
┃ ┃ ┗ 📜index.ts
┃ ┣ 📂types
┃ ┗ 📂utils
┣ 📂pages
┃ ┣ 📂agreement
┃ ┣ 📂creator
┃ ┣ 📂error
┃ ┣ 📂manager
┃ ┃ ┣ 📂[AdsId]
┃ ┃ ┗ 📜index.tsx
┃ ┣ 📜_app.tsx
┃ ┣ 📜_document.tsx
┃ ┗ 📜index.tsx
┗ 📂shared
┃ ┣ 📂apis
┃ ┣ 📂assets
┃ ┃ ┣ 📂pngs
┃ ┃ ┣ 📂svgs
┃ ┣ 📂components
┃ ┃ ┣ 📂Button
┃ ┃ ┣ 📂Input
┃ ┃ ┣ 📂access-control
┃ ┃ ┣ 📂badge
┃ ┃ ┣ 📂banner-message
┃ ┃ ┣ 📂calendar
┃ ┃ ┃ ┣ 📂utils
┃ ┃ ┣ 📂checkbox
┃ ┃ ┣ 📂dialog
┃ ┃ ┃ ┣ 📂Modal
┃ ┃ ┣ 📂error
┃ ┃ ┣ 📂filter
┃ ┃ ┣ 📂grid
┃ ┃ ┣ 📂layout
┃ ┃ ┣ 📂loading
┃ ┃ ┣ 📂navigation
┃ ┃ ┣ 📂page-title
┃ ┃ ┣ 📂pagination
┃ ┃ ┣ 📂radio
┃ ┃ ┣ 📂seo
┃ ┃ ┗ 📜index.tsx
┃ ┣ 📂constants
┃ ┣ 📂context
┃ ┣ 📂hooks
┃ ┣ 📂types
┃ ┣ 📂utils
- 총 15000줄이다...
개선점 + 대안
- 이번 프로젝트를 하면서 아쉬웠던 점 3가지를 뽑자면 다음과 같습니다.
- 어떤 내용을 어떻게 개발할지에 대해 설계하지 않은 점
- 저의 초기 스타트업 개발자라는 뿌리가 다시 발현되었습니다. 초기 스타트업 특성상 설계할 시간에 빠르게 유저에게 보여주고, 이걸 계속 가져갈지 버릴지를 판단하는 작업이 수없이 많이 이뤄지기 때문에, 설계할 시간에 MVP 만들어서 유저에게 제품을 보여줘야 합니다.
- 그러나, 다시한번 생각해보면 지금 텀블벅에서의 광고 플랫폼이라는 프로젝트 상황도 크게 다르지 않았고, 상황에 맞는 올바른 개발을 했다는 생각이 들었습니다.
- 개인적으로 부족하다고 생각하는 것이 단단한 설계를 기반으로한 개발인데, 앞으로 진행할 대시보드 프로젝트, 특히 차트와 같은 복잡한 로직을 많이 구현하는 부분에선 설계를 단단히 해보려고 합니다. 텀블벅이 원래는 디자인 독스를 기반으로 개발을 해왔다고 해서, 디자인 독스를 통해 전체적인 맥락을 잡아서 개발을 해봐야겠습니다. 그리고 나에게 맞는 개발 방식을 채택해서 나아가야겠습니다.
- 코드 리뷰의 단위
- 위의 어려웠던 점에서 설명했던 광고 설정 부분을 처음 개발에 도입할 때, 도무지 감이 잡히지 않았습니다. 그래서 일단 개발해보면서 감을 잡으려고 하다보니, 결국 전부 개발을 했을 때, 1600줄이 넘는 코드가 작성되었습니다. 이렇게 됐을때의 문제가 팀원들의 코드 리뷰가 힘들어지고, 그만큼 내가 놓치는 부분이 많아질 가능성이 커지기 때문에 좋은 방식은 아니였습니다.
- 그래서 앞으로는 가능하면 최대한 잘게 쪼개서 개발을 착수하고, 위의 상황처럼 전혀 감이 잡히지 않는 상황에서는 500자가 넘지 않는 선에서 중간에 코드리뷰를 받음을 통해 너무 많은 양의 코드를 PR에 올리는 것을 최대한 지양해야겠습니다.
- 개발만 하느라 팀원들과 충분한 친밀감을 쌓지 못습니다.
- 아무래도 꽤나 커 보이는 산이 앞에 놓여있다 보니, 마음이 계속 편하지만은 않았던 것 같습니다. 그래서 심리적으로 여유가 없어지고, 목표에 몰입하다 보니 1달 반동안 정말 개발만 하거나 프로젝트와 관련된 얘기만 한 것 같습니다. 그러면서 자연스레 긴장인 상태가 오래 지속되었던 것 같습니다.
- 긴장인 상태가 지속됐을 때, 쉽게 지치게 되고 피로도가 쉽게 쌓이게 됩니다. 그래서 요새는 팀원들한테 사소한 얘기라도 말을 걸려고 노력하고, 속보다는 밖으로 얘기를 많이 꺼내려고 노력합니다. 원래는 쓸데없는 말을 하기 싫어하는 타입인데, 요새는 쓸데없는 말은 없다는 생각으로 이것저것 얘기를 많이 꺼내려고 노력합니다.
- 그러다보니, 자연스레 업무에 대한 내용이 공유되고 생각 자체가 투명하게 공유되서 의견을 나누기가 훨씬 수월해진 것 같습니다. 여러모로 일하기 좋은 개발자가 되기 위해 부단히 노력하고 있는 중입니다.
결론
결과적으로 2월 초부터 개발에 착수하여 3월 15일까지 개발을 완료할 수 있었습니다. 회고할 때, 어떤 데이터 개발자 분이 말씀하시길 "솔직히 프론트 개발자분들이 전부 오프보딩하는걸 보면서, 프로젝트가 무산될 줄 알았어요.."라고 말씀하시는 것을 보면서, 큰 뿌듯함을 느꼈습니다.
CTO님께서 감동했다는 표현을 쓰셨습니다. 저도 덩달아 감동했습니다 ㅠ.ㅠ
운전을 하다가 긴 터널에 들어가면 끝에있는 출구의 빛이 안보이듯이, 프로젝트를 처음 시작할 때는 빨리 빛이 보이길 바라는 마음으로 했던 것 같습니다.
광고 플랫폼을 완성하고 기쁜 마음에 사내 슬랙을 통해 시연 영상을 공유하였고, 엔지니어분들 뿐만 아니라 많은 분들이 좋아해주셔서 프론트엔드 엔지니어로써 뿌듯함을 느꼈습니다. 그래서 다시한번 나는 프론트가 맞구나.. 라고 생각하게 되었습니다.
역시 마음먹으면 되지 않는 일은 없습니다.
내 실력이 많이 늘었다는 생각을 하게 되었습니다. 텀블벅에 입사하기 이전에 불가피하게 이직하는 시간을 가지게 되었고, 그 기간이 길수록 나는 힘들지만 내 실력은 기하 급수적으로 늘어난다고 생각했었습니다. 정말 피곤한 생각인데, 지금 돌아보면 꽤나 괜찮은 생각이었을지도 ?
현재 QA가 진행중인데, 생각보다 QA가 많이 나오지 않아서, 그래도 꽤 잘 만들었나? 라는 생각이 듭니다.
팀원들에게 코드리뷰를 계속해서 받게되면서 자연스럽게 다음 사람이 개발하기 편한 코드 혹은 이해하기 쉬운 코드의 관점에서 고민하다보니, 자연스럽게 코드에도 그게 드러난 것 같아서 기쁩니다.
처음에 프로젝트를 맞닥뜨렸을 때, 두려운 마음이 컸는데 그런 부정적인 감정에 집중하지 않고, 어떻게 개발할지에 대해서 생각했던 점에서 내가 정신적으로도 꽤나 성장했다고 느껴지는 부분이었습니다.
컴포넌트의 재사용성에 대한 집착을 버림을 통해 더 읽기 좋은 코드를 짤 수 있었습니다.
제가 그동안 사용하던 리액트 디자인 패턴은 atomic design pattern과 control props pattern 이었습니다. 이번에는 기존에 짜여진 프로젝트 구조대로 shared component만 분리하고, pages별로 컴포넌트를 만들었는데, 꽤나 편리하고 관리하기 편했습니다. 그래서, 이론을 적용하는 것도 좋지만, 나의 상황에 알맞게 적용해야 한다는 점을 배웠고, 맹목적으로 이론에 따라가다, 실제 나의 개발 환경의 질을 떨어뜨릴 수 있겠다는 생각을 하게 되었습니다.
하나의 산을 넘은 것 같아서 기쁩니다. 광고 플랫폼이 잘 출시가 된 이후에는, 약간의 텀을 가지면서 다음에 다가올 챌린지를 대비해야 겠습니다. 앞으로는 나에게 더 큰 챌린지가 주어졌으면 좋겠는 약간의 욕심(?)이 있습니다. 그러나, 언제나 그렇듯 챌린지는 고통스럽습니다. 🥲 지금껏 그래왔듯이 앞으로도 나의 기대치를 낮추지 않고 계속해서 챌린지하는 챌린저가 되고싶습니다.
마지막으로 제가 요새 굉장히 꽂혀있는 단어에 대해 공유하고 마치겠습니다. 제가 요새 가장 꽂혀있는 단어는 “용기”입니다. 고대 그리스어로 용기는 “andreia”라고 하는데, 놀랍게도 이 용기의 반대말은 무기력함 이라고 합니다. 따라서, 계속해서 용기내어 도전하는 삶 사시면서 모두 행복한 삶 영위하셨으면 좋겠습니다.