본문 바로가기
펀잇

우아한테크코스 프로젝트 기획

by 해-온 2023. 8. 13.

우아한테크코스 레벨 3가 시작됐다.

레벨 3에는 프로젝트를 하게 된다.

 

프로젝트 주제는 사전에 선정이 되고 랜덤 배정이 되는데

나는 편의점 상품을 리뷰하는 서비스에 배정되었다.

 

 

우리는 2주 정도 기획에 힘을 쏟았다.

기획을 잘해두면 방향성을 잃지 않고 갈 수 있을 것이라 생각했다.

 

다양한 방법과 접근을 통해 우리 서비스의 길을 정하기로 했다.

그 과정을 소개해보려고 한다.

 


🎁 기획

🥄 킥오프 미팅

킥 오프 미팅을 통해 프로젝트의 배경과 목표, 기능을 정했다.

 

먼저, 배경은 다음과 같다.

 

  • 편의점 수가 늘어나며 사용자 수 급증
  • 편의점에서 자체 개발한 PB 상품 등장
  • 편의점 음식 리뷰는 광고인 경우가 많음
  • 리뷰들이 여러 플랫폼에 걸쳐 있어 모아 보기 힘듦

 

요즘 외식 물가가 올라가며 편의점 외식 시장이 급 성장했다.

 

이를 통해 프로젝트의 목표를 편의점에서 음식을 구매하는 사람들이

리뷰를 편하게 확인할 수 있도록 하는 것으로 설정하였다.

 

서비스를 이용하여 나올 수 있는 큰 기능은 다음과 같다.

 

  • 편의점 음식 목록 확인
  • 편의점 음식 리뷰
  • 편의점 음식을 활용한 레시피 공유

🥄 페르소나

페르소나 설정을 통해 어떤 유저가 우리 서비스를 주로 사용할 것인지에 대해 이야기를 해보았다.

 

여러 크루들을 인터뷰해 본 결과 두 종류의 유저로 나눌 수 있었다.

 

먼저 눈팅러, 21살의 대학생이고 자취를 하고 있다.

음식 하기 귀찮아하고 편의점 음식을 자주 사 먹는다.

하루에 한 끼는 편의점 음식으로 식사하고, 가성비를 기준으로 소비한다.

매일 먹는 참치 삼각김밥에 질려 새로운 편의점 음식에 도전하고 싶어 하지만,

결국 고르기 귀찮아 똑같은 음식을 먹게 된다.

 

다음으로는 편진러, 29살의 백수로 자취를 하고 있다.

대학생 때 편의점 음식을 자주 먹어 꿰뚫고 있다.

SNS 중독자로 정보를 제시하고 사람들의 반응을 즐기고 있다.

편의점 가기 전에 어떤 음식을 먹을지 찾아본다.

 

이 두 페르소나를 바탕으로 유저 시나리오를 작성하였다.

 

자취하는 21세 학생 눈팅러는 배가 고프다. 

저녁을 먹고 싶은데 음식을 해 먹기는 귀찮고 배달하기에는 돈이 없다.

그래서 편의점 음식으로 간단하게 때우기로 결정했다.

 

맨날 같은 참치 삼각김밥을 먹다 보니 질렸다.

그래서 어떤 다른 음식이 있는지 보고 싶다.

‘펀잇’의 상품 랭킹을 통해 요즘 어떤 게 맛있는지 확인한다.

2위에 빛나는 ‘우아한 땡초김밥’의 상세 정보를 확인한다.

어떤 맛인지 궁금해 밑에 달린 별점과 리뷰를 확인한다.

맛있어 보여서 편의점에서 사 먹는다.

 

기대와 다르게 맛이 그저 그래서 별점 3점과 ‘나 같은 취향을 가진 사람에게는 그저 그럴 수 있다’는 리뷰를 단다.

 

29세 편진러는 땡초김밥이랑 조합해 볼 수 있는 레시피를 고민한다.

이전에 참치 마요 컵라면과 함께 먹었던 기억이 있어, 해당 조합을 꿀조합 게시판에 업로드한다.

그는 해당 레시피로 많은 맛있어요 수를 받고 뿌듯해한다.

 

이렇게 유저 시나리오를 작성하고 나니, 우리 서비스는 어떤 사람이 주로 이용하고

어떤 기능을 중점적으로 두어야겠구나 하는 틀이 나왔다.

이를 바탕으로 상세 명세를 작성할 수 있었다.

 

🥄 기능 명세서

이제 상세 명세를 정해보자.

큰 기능인 3가지를 주축으로 하고, 이에 더 필요한 기능을 더했다.

 

  • 상품 목록을 볼 수 있는 ‘상품’
  • 상품을 검색할 수 있는 ‘검색’
  • 상품에 대한 평가를 할 수 있는 ‘리뷰’
  • 꿀조합을 만들고 볼 수 있는 ‘레시피’
  • 로그인을 할 수 있는 ‘회원’‘마이페이지’

상품

상품의 경우 크게 ‘공통 상품’‘pb상품’으로 나뉜다.

그리고 카테고리별로 볼 수 있는데, 모든 카테고리보다는 

식사 대용으로 주로 먹는 것, 조합해 볼 수 있는 카테고리를 선택했다.

 

사용자는 상품 목록을 정렬하고, 검색할 수 있다.

또, 리뷰를 바탕으로 계산된 가중치를 통해 선정된 음식 랭킹을 확인할 수 있다.

 

해당 상품을 클릭해 세부 정보를 확인할 수 있다.

편의점 음식을 북마크 해서 보관할 수 있으며,

남긴 리뷰를 총합해 상위 3개의 대표 태그를 볼 수 있다.

 

리뷰

사용자는 리뷰를 작성할 수 있다.

별점과 태그, 사진, 글, 재구매 의사를 통해 보다 다채롭게 평가할 수 있다.

 

사용자는 리뷰를 볼 수 있다.

평점 높은/낮은 순, 추천순, 최신순으로 리뷰를 정렬해 볼 수 있다.

마음에 드는 리뷰는 추천을 눌러 반응을 남길 수 있다.

 

레시피

사용자는 레시피를 작성할 수 있다.

사용한 상품들을 입력하고, 어떻게 조리해야 하는지 적어 공유할 수 있다.

인기 있는 레시피는 상위에 랭크되어 쉽게 접근할 수 있다.

 

회원

사용자는 서비스에 가입할 수 있다.

사용자는 가입하지 않고 페이지에 진입할 수 있으나,

리뷰와 레시피 확인/작성은 불가능하다.

 

마이페이지

사용자는 자신이 작성한 리뷰와 레시피를 확인할 수 있고

북마크 한 상품과 레시피를 확인할 수 있다.

 

 

기능 명세를 바탕으로 스토리 매핑을 했다.

 

이렇게 구체적으로 기획을 해두니, 어떻게 구현해야 할지 쉽게 이해할 수 있었고

레벨 3가 마무리되는 지금까지 크게 변경 없이 그대로 진행하고 있다.

시간이 없어서 레벨 4에 구현하는 북마크 빼고는 그대로인 듯?

 


🚜 개발 환경

 

다음으로는 우리 팀의 개발 환경에 대해 소개하겠다.

개발 환경 역시 기획 못지않게 중요하다.

 

팀의 규칙이 없으면 분위기가 흐려지고, 소통이 안 되는 문제가 생기기 때문이다.

그러면 당연히 프로젝트는 산으로 가게 된다.

 

따라서 우리는 팀 문화 또한 상세하게 정했다.

 

🥄 팀 공통 목표

먼저 팀 공통 목표이다.

 

  • 체계적인 협업 프로세스 (일정 관리 등)
  • 문서화 잘하기 (회의록, 트러블슈팅 등)
  • 기능이 적어지더라도 기술을 깊게 파보기

 

서로 프로젝트에서 얻어갔으면 하는 것을 정리하고 공통적인 부분을 취합했다.

개발을 진행하면서 위 3가지는 잊지 않고 해 보기로 결정했다.

 

🥄 데일리 미팅

매일 2번의 데일리 미팅을 통해 오늘의 기분이 어떤지, 오늘 뭘 하고 뭘 했는지 공유하기로 했다.

그날의 진행자는 가위바위보를 통해 결정하고, 20분 내외로 가볍게 진행한다.

 

확실히 데일리 미팅을 진행하니 시작하기 전에 리프레쉬할 수 있고,

스트레스를 덜 받게 된다.

우리는 매일 데일리 미팅이 끝나면 다 같이 ‘꼬맨틀’을 하면서 언어 능력(?)까지 키우고 있다.

 

🥄 생활

정해진 교육 시간 (10~18시)에는 팀 프로젝트 관련 일만 하기로 했다.

물론, 근로나 스터디할 수는 있지만 준비는 개인 시간에 하기로 했다.

 

주말과 공휴일, 야근은 자율로 하고 강요하지 않는다.

 

정해진 코어 시간이 있고, 그 시간에는 함께 모여서 개발을 한다.

우리 6명 모두 쉬는 시간을 필요로 했는데, 그러려면 코어 시간이 있어야 집중할 수 있다는 결론이 나왔다.

근데 코어시간은 아무도 지키지 않아 자연스럽게 팀 문화에서 사라졌다. ^^…?

 

코어 시간 없이도 다들 제 자리에 잘 앉아 있었고, 소리 소문 없이 그런 게 있었지! 하고 잊어버렸다.

그래서 그냥 없애기로 했다!

 

지각 등 문제 행동 시 커피 쿠폰이 적립되고, 우리 인원수인 7잔을 채우면 다 같이 커피를 사 먹는다.

기여한 만큼 나눠서 결제를 한다.

 

🥄 회의

회의는 크게 2가지로 나눠진다.

주간회의긴급회의가 있다.

 

주간회의는 매주 하는 회의로 화요일 14~15시로 정해져 있다.

이번주 목표와 저번주 목표 달성률을 공유하고

해결된/해결할 문제를 공유한다.

같이 논의해야 할 내용이 있다면 발표한다.

 

긴급회의는 말 그대로 긴급으로 진행하는 회의이다.

문제를 인식한 사람이 안건지를 작성해 오고

3명 이상 이야기하면 회의를 진행하고, 회의록을 작성한다.

 

회의 사회자는 우리 닉네임의 글자순으로 진행하고,

다음 글자순의 크루가 서기를 담당한다.

 

회의 중에는 존댓말을 사용하기로 했는데, 이것 역시 코어 타임처럼 슬며시 사라졌다.

 

🥄 기록

모든 정보를 한 곳에 모은다.

노션에 1차적으로 작성하고, 최종적으로 wiki에 작성한다.

 

데일리 미팅과 같이 가벼운 내용은 노션에 작성하고,

회의록이나 트러블 슈팅은 wiki에 작성한다.

 

매 스프린트마다 크루별로 최소 글 하나씩 작성해 올린다.

 

🥄 기타

이 외의 사항은 기타 항목으로 정리하였다.

🥄 git 전략

브랜치 전략

브랜치는 총 5개로 나누었다.

 

  • 배포 브랜치인  main
  • 개발 브랜치인 develop
  • 기능 개발 브랜치인 feature
  • 기능 수정 브랜치인 fix
  • 긴급 버그 수정 브랜치인 hotfix

 

develop을 default로 하고, 그 하위에서 파생하는 방식으로 진행하였다.

브랜치 명은 ‘feat/issue-이슈번호 | fix/issue-이슈번호’,

커밋은 ‘feat: 커밋 메시지 내용’으로 고정하였다.

 

커밋 메시지를 줄이기 위해 feature에서 develop으로 갈 땐 squash merge를 하고,

커밋 이력을 동일하게 하기 위해 develop에서 main으로 갈 땐 merge commit을 한다.

 

배포 서버와 개발 서버로 나누고, 버저닝이 가능한 상태가 되면

develop에서 main으로 merge 한다.

 


📖 기술 문서

프론트엔드 내부에서의 개발 문서를 소개해보려고 한다.

 

📚 기술 스택

타입스크립트 기반의 리액트를 사용한다.

CRA 하지 않고, 웹팩을 사용해 직접 리액트 환경을 구축한다.

 

CSS의 경우 styled-components를 사용한다.

styled-components는 props를 받아 동적 스타일링이 가능하고,

스타일과 마크업이 같이 존재해 가독성이 높고 유지보수가 쉽다.

물론, 런타임에 CSS 코드를 생성해 CSS in CSS 보다는 속도가 느리지만

서비스 크기가 크지 않기 때문에 유의미한 차이는 보이지 않을 것이라 판단해 선택했다.

 

보통 편의점으로 이동하면서 간편하게 볼 수 있게 하기 위해 모바일을 우선적으로 지원하기로 결정했다.

 

📒 코드 컨벤션

export 방식

  • 컴포넌트는 export default
  • hook은 export 해서 index.ts에 정의하기
//correct
const Component=()=> {...};
export default Component;

export const func =()=>{...};

export const useHook=()=>{...};

//incorrect
export const Component=()=> {...};

이벤트 핸들러 네이밍

  • 어떤 동작을 하는지 명시적으로 작성하기
  • 네이밍: handle+동작(동사)
    • ex) handleLogin, handleRegisterForm

스타일 순서

display
	align-items
	justify-contents
position
	top right bottom left
float
width
height
margin
padding
border
background
font
color
text-decoration
text-align / vertical-align
white-space
other text
content
가상 선택자

styled-components

  • 한 파일에서 마크업+스타일 정의
  • 가장 큰 묶음은 XXContainer , 자잘한 것들은 XXWrapper

if문

  • 한 줄 이더라도 중괄호 필수
//correct
if (ok) {
	return true;
}

//incorrect
if (ok) return true;

나머지 props 네이밍

const Component = ({ color, children, ...props }) => { ... }

스타일 분기할 때

const dividerSizeStyles = {
  'default' : css ``,
}

const dividerColorStyles = { ... };

storybook description

서술형으로 쓰기

color: {
	description: '컴포넌트의 색상입니다.'
}

style은 export 아래에

export default Component;

const Container=styled.div``;

interface에 jsdocs 추가

interface Props {
/**
 * 컴포넌트의 색상입니다.
 */
color?: Colors
/**
 * 컴포넌트의 길이를 나타냅니다.
 */
length?: string
}

컴포넌트의 타입 → variant로 네이밍 고정

interface Component {
	variant: 'outlined' | 'filled'
}

storybook meta

const meta:Meta<typeof Comonent> = { ... }

CSS Props

<Box
  css={`
    font-size: 16px;
  `}
/>;

StyleProps

interface ComponentProps {
	variant:
	border:
	color:
}

type StyleProps = Pick<ComponentProps, 'variant'| 'border' | 'color'>;

폴더 구조

📦src
 ┣ 📂apis
 ┣ 📂assets
 ┣ 📂components
 ┣ 📂constants
 ┣ 📂hooks
 ┣ 📂mocks
 ┣ 📂pages
 ┣ 📂router
 ┣ 📂styles
 ┣ 📂types
 ┣ 📂utils
 ┣ 📜App.tsx
 ┗ 📜index.tsx

 

📘 TypeScript 컨벤션

🥄 우리 팀에서 typescript를 쓸 때 중요하게 생각하는 부분

  • 최대한 타입 추론을 사용하기
  • 중복되는 타입을 줄이기 위해 유틸리티 타입 사용하기

🥄 Component

선언방식 - 함수 표현식

//correct
const Component = () => {...};

 

Props - 컴포넌트 이름+Props

//correct
interface HeaderProps {};

//incorrect
interface Props {};

 

Props로 명시했을 때 모든 컴포넌트의 Props가 검색되어 찾기 어렵다.

따라서 컴포넌트 이름+Props로 정의해 보다 직관적으로 명시한다.

 

type vs interface - 유니온 타입은 type, 나머지는 interface 사용

//correct
type T = 'type' | 'example';
interface SomeThing {};

//incorrect
type T = {};

 

interface의 경우 확장이 가능하다.

 

Component with Children / Component without Children

  • 컴포넌트 만들 때 ComponentPropsWithoutRef
  • children 받을 때 PropsWithChildren
    • 리액트 표준으로 제공해주고 있고 간편하게 쓸 수 있다.

Event

// correct
const handleClick: MouseEventHandler<HTMLButtonElement> = (event) => { /*...*/ };

// incorrect
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => { /*...*/ };

 

  • 이벤트 자체의 타입보다는 함수의 타입을 지정해 명시적으로 쓰고 싶어서 사용했다.

🥄 Hooks

기본 훅

const [data, setUser] = useState<User>();

 

  • 현재까지는 위의 방식으로 쓰고 있지만 문제가 생긴다면 변경할 예정

Ref

const ref = useRef<HTMLElement>(null);
  • 현재까지 ref는 돔객체에 접근하기 때문에 null로 초기화

🥄 모듈

type 관리 방식

  • types 폴더 아래에 common, products, review 등의 폴더를 만들어 사용
  • 다른 타입들에 대해서는 딱히 prefix나 suffix를 사용하지 않음

type import/export

//correct
import type { Type } from '@/types/common';

//incorrect
import { Type } from '@/types/commmon';
  • lint를 사용하여 자동으로 type import가 되게끔 설정

🥄 API

Request / Response Type

 

  • api response의 경우 CategoryProductResponse와 같이 네이밍을 지음
const useCategoryProducts = (categoryId: number, sort: string = PRODUCT_SORT_OPTIONS[0].value) => {
  return useGet<CategoryProductResponse>(
    () => categoryApi.get({ params: `/${categoryId}/products`, queries: `?page=1&sort=${sort}` }),
    [categoryId, sort]
  );
};

🥄 빌드 설정

loader

  • ts-loader 사용
    • babel-loader가 속도는 더 빠르지만 타입 체킹을 하지 않는다는 점과 폴리필이 필요한 IE가 지원 종료되었다는 점을 고려하여 ts-loader로 결정함

tsconfig

{
  "compilerOptions": {
    "target": "esnext",
    "lib": ["dom", "dom.iterable", "esnext"], //컴파일 과정에서 사용되는 JS API와 같은 라이브러리를 지정
    "esModuleInterop": true,
    "module": "esnext", //컴파일을 마친 자바스크립트 모듈이 어떤 모듈 시스템을 사용할 지
    "moduleResolution": "node",  //node전략을 사용하여 모듈 탐색
    "resolveJsonModule": true, //json 파일 import 허용
    "forceConsistentCasingInFileNames": true, //파일 이름의 대소문자 구분
    "strict": true,
    "skipLibCheck": true, //라이브러리에 대해서는 타입 체킹x (컴파일 시간 줄일 수 있음)
    "jsx": "react-jsx", //tsx를 어떻게 컴파일 할 지 방식을 정함
    "outDir": "./dist",
    "typeRoots": ["node_modules/@types", "src/types"], //타입의 위치 지정
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  },
  "include": ["src"], //컴파일에 포함
  "exclude": ["node_modules", "dist"] //컴파일에 제외
}

 


이렇게 2주 정도에 걸친 기획을 기술해 보았다.

거의 매일 회의를 해서 몰랐는데, 정리해 보려니까 양이 많다.

 

그래도 제대로 기획을 해둔 덕분에 개발에만 집중할 수 있었다.

길을 잃지 말고 그대로 마무리까지 잘하자!

 

펀잇 짱 🥄

 

댓글