본문 바로가기

프로젝트/[Next.js& FireBase] 바텐더 쇼핑몰

[Next.js & FireBase] 프로젝트 매니지먼트용 문서 작성, 구글 Stitch가 만들어준 UI 옮기기, 상품 더미 데이터 입력하기

 

프로젝트 매니지먼트용 문서 작성

 

 

프로젝트 매니지먼트 연습

- 사실 개인 개발이면 프로젝트 매니지먼트에 힘을 크게 쏟을 필요가 없긴 하다. 오히려 그러다가 배보다 배꼽이 더 커져서 개발이 늦어지는 경우도 있기 때문.

- 하지만 작년 7월부터 본격적으로 회사에서 개발자 일을 시작하면서, 프로젝트를 체계적으로 관리하는 것이 꽤나 중요하다는 것을 깨달았고, 꽤 적당한 느낌으로 개발을 해온 나로서는, 체계적으로 프로젝트를 진행해나가는 연습이 필요하다고 느꼈다.

- 그래서 요전에 진행하다가 엎어진 게임 프로젝트에서도 태스크 관리 시트는 만들었지만, 이번에는 간트 차트를 비롯해, 태스크 관리 시트를 제대로 작성할 수 있도록 엑셀 파일을 작성해보았다.

- 혼자 진행하는 프로젝트지만 확실히 태스크 진행같은걸 잘 정리하니 이제 뭐해야하지 하는 뇌정지가 덜 오는 느낌이 있다.

 

 

태스크 관리 시트

- 태스크를 전반적으로 관리하는 테이블이다.

- 새롭게 처리해야할 태스크가 생기면 테이블에 행을 추가하고, '분야'와 '제목'을 작성한다. '분야'는 아래와 같이 분류했다.

설계(DES) 말그대로 설계 및 아키텍처 관련된 태스크.
비주얼(VIS) UI 리소스를 준비하거나 UI면에서의 구현과 관련된 태스크.
데이터처리(DAT) 데이터와 데이터베이스 처리에 관련된 태스크.
구현(DEV) 기능의 로직이나 프로세스를 구현하는 태스크.
디버그(DEB) 디버깅 태스크.
리팩토링(REF) 리팩토링 태스크.
매니지먼트(MAN)
프로젝트 매니지먼트 관련 태스크.
그 외(ETC) 그 외.

- 테이블 우측엔, 분야를 선택하면 자동으로 ID를 입력해주는 액션 스크립트를 집어넣은 버튼을 배치했다.

 

 

간트 차트 시트

- 간트 차트는 태스크 진행이 일정대로 되고 있는지를 한 눈에 보기 위한 그래프같은 것인데, 위의 태스크 관리 시트를 참조해 간트 차트가 그려지도록 만들었다.

- 노란색은 예정 일정, 초록색은 실제 완료한 일정으로, 일정대로 되고 있는지를 바로 체크할 수 있다.

- 사실 더 제대로 만들고 싶긴 했는데 이 정도로도 충분해서 이 작업은 마무리지었다.

 

 

개발일지 시트

- 가볍게 매일매일 작업을 기록하는 용도로 만든 시트다.

- 오늘 무엇을 했고, 내일 무엇을 할지를 바로 확인할 수 있고, 일기장처럼 감상을 남길 수 있게 해두었다.

 

 

 

 

 

구글 Stitch가 만들어준 UI 옮기기

 

 

구글 Stitch가 만들어준 UI

- 구글 Stitch가 이런 느낌으로 UI를 만들어주었고, HTML 코드도 쓸 수 있게 해주었다.

- 이제 이걸 그대로 받아서 쓰면 되는데, 문제는 페이지마다 통HTML로 되어있어서 프로젝트에 맞게 재구성해주는 것이 중요하다.

- 현재 내 프로젝트의 경우엔 아직 컴포넌트를 어떻게 나눌 지 생각을 안해놨기 때문에, 헤더 - 본문 - 푸터 구성으로만 일단 나눠주기로 했다.

 

 

프로젝트에 TailWind 적용하기

- 구글 Stitch에게 받은 HTML코드는 TailWind로 스타일링이 되어있었다.

- TailWind는 CSS프레임워크로, CSS파일을 따로 작성하지 않아도 요소에 이미 정의된 유틸리티 클래스를 조합해 적용함으로써 빠르고 간편하게 스타일을 적용할 수 있게 해준다.

- 한편으론 요소마다 각각 CSS를 인라인으로 적용하는 것처럼 해야하다보니 유지보수하기 힘들지 않을까 했는데, Next.js는 어차피 컴포넌트를 조합해 페이지를 구성하는 방식이다보니, 컴포넌트를 잘 나눠줌으로써 이 문제를 해결할 수 있는 것 같다.

 

- 기존 프로젝트에 TailWind를 설치하고 적용하려면 터미널에 다음 커맨드를 입력하면 된다.

// TailWind 설치
npm install tailwindcss @tailwindcss/postcss postcss
// TailWind 초기화
npx tailwindcss init

 

- 그리고, postcss.config.mjs 와 globals.css 파일을 아래와 같이 수정해준다.

// postcss.config.js
const config = {
  plugins: {
    "@tailwindcss/postcss": {},
  },
};

export default config;

// globals.css
@import "tailwindcss";

 

- 이렇게 설정해주면 TailWind를 사용해 스타일을 지정할 수 있게 된다. 덧붙여, Visual Studio Code 확장 프로그램으로 TailWind Intellisense까지 설치해준다.

 

 

구글 Stitch가 만들어준 UI 그대로 옮기기

- 구글 Stitch는 HTML코드를 그대로 출력해서 주지만, 이 프로젝트는 Next.js이기 때문에 JSX코드로 바꿔주어야 한다.

- UI를 옮기는 것 자체는, 'class'를 'className'으로 바꾸는 것과, 일부 프로퍼티를 카멜 케이스로 바꾸는 것으로 충분했다.

- 구글 Stitch가 그려준 UI(좌) 와, 그대로 코드를 옮긴 결과(우). 폰트나 일부 요소에 약간의 차이는 있지만 그대로 가져올 수 있었다. (이미지는 왜 바뀐거지?)

 

 

 

 

상품 더미 데이터 입력하기

 

 

DB 설계 수정

- 주요 변경점은 다음과 같다.

  1. Products 컬렉션에 필드 추가

  : 상품 검색, 필터링 기능을 위해, category(대분류), subcategory(소분류), tags(태그) 필드 추가.

  : 리뷰 기능을 깜빡했다. reviewAvg(평균 별점), reviewCount(리뷰 갯수) 추가.

  : 상품 상세 정보에서 여러 이미지를 표시할 수 있도록 images를 배열로 설정.

 

  2. Products의 다언어 지원

  : 이 부분에서 고민이 많았다. 밑에서 자세히 이야기하겠다.  

 

  3. Review 컬렉션 추가

  : 리뷰 기능을 위해 추가.

 

 

다언어 지원을 하기 위한 Products 컬렉션 수정

- 생각해보니, 다언어 지원까지 염두에 두고 있다 보니, Products 컬렉션의 필드들도 다언어 지원이 가능하도록 하려면 어떻게 해야할 지 생각을 하게 되었다.

- 우선, 다언어 지원이 필요해보이는 필드는 다음과 같다.

1. name : 상품 이름

2. category : 대분류

3. subcategory : 소분류

4. brand : 브랜드

5. tags : 태그

6. description : 상품 상세 설명

 

- 이것들을 어떻게 대응할 지에 대한 결정은 다음과 같다.

  1. string → 객체 타입으로 변경

  : string 타입이던 name, description을 언어별로 string 데이터를 가진 객체 타입으로 변경. 

 

  2. 영어로 통일

  : category, subcategory, tags의 경우, 이 필드의 데이터를 이용해 필터링이나 검색과 같은 기능을 지원해야 하기 때문에, DB상에는 영어로 저장해놓고, 화면에 표시할 때에만 클라이언트 단에서 번역을 하기로 했다.

 

  3. 그냥 영어로 설정

  : brand는 그냥 영어로만 설정하기로 했다.

 

- DB의 이야기는 아니지만, '구매하기', '회원 가입'과 같이, 화면에 나타나는 메세지 역시 다언어 지원을 해야하는데, 이러한 메세지들은 /locales/ko.json, en.json, ja.json 이런 식으로 언어별로 json파일을 만들어 저장해놓고, next-intl과 같은 라이브러리를 사용해 대응할 수 있다고 한다.

 

 

LLM으로 더미 데이터 작성하기

- 상품 목록 화면을 만들기 위해, 우선 상품 데이터가 필요한데, 하나하나 입력하는건 번거로우니 구글 Gemini를 통해 더미 데이터를 작성했다.

- 아래는 ChatGPT를 통해 작성한 더미 데이터 작성 프롬프트와, 그 결과로 나온 더미 데이터 일부이다.

바텐더를 위한 쇼핑몰 웹사이트의 Firestore용 더미 Products 데이터를 JSON 배열 형태로 30개 생성해줘.
출력은 반드시 JSON 배열만 출력하고, 설명 문장은 포함하지 말 것.
다음 조건을 반드시 지켜줘:

1. 각 객체는 다음 필드를 가져야 한다:
- productId (문자열, "p001" ~ "p030" 형식으로 순차 생성)
- category (문자열, 아래 영문 코드 중 하나)
- subcategory (문자열, category에 맞는 영문 소분류 코드)
- brand (영문 브랜드명)
- price (엔화 기준 정수, 상품 유형에 맞는 현실적인 가격)
- stock (0~100 사이 정수, 일부 상품은 0 포함)
- rateAvg (숫자, 기본값 0)
- rateCount (숫자, 기본값 0)
- tags (영문 문자열 배열, 최소 3개 이상)
- images (문자열 배열, 2~4개, 다음 형식 사용:
  "https://picsum.photos/seed/{productId}-{n}/600/600")
- name (객체 형태, 다음 구조)
    {
      "ko": "...",
      "en": "...",
      "ja": "..."
    }
- description (객체 형태, 다음 구조)
    {
      "ko": "...",
      "en": "...",
      "ja": "..."
    }

2. category는 반드시 아래 4가지 영문 코드 중 하나만 사용:
- "liquor"
- "ingredient"
- "tool"
- "accessory"

3. subcategory는 반드시 category에 맞는 영문 코드로 작성:
예시)
- liquor: whiskey, rum, gin, vodka, tequila, triple-sec, liqueur
- ingredient: lemon, lime, sugar-syrup, apple-puree, tonic-water, soda-water, bitters
- tool: shaker, jigger, bar-spoon, strainer, scale, apron
- accessory: recipe-book, glass-set, bar-mat, garnish-kit

4. name과 description은 바텐더를 타겟으로 한 전문적인 분위기로 작성하고,
   ko, en, ja 각각 자연스럽게 번역된 형태로 생성할 것.

5. tags는 검색 및 필터링에 적합한 영문 키워드로 작성할 것
   (예: smoky, premium, citrus, bar-essential, professional, handcrafted 등)

6. category는 4가지가 가능한 한 균등하게 분포되도록 생성할 것.
[
  {
    "productId": "p001",
    "category": "liquor",
    "subcategory": "whiskey",
    "brand": "Macallan",
    "price": 9500,
    "stock": 45,
    "rateAvg": 0,
    "rateCount": 0,
    "tags": [
      "premium",
      "single-malt",
      "speyside",
      "woody"
    ],
    "images": [
      "[https://picsum.photos/seed/p001-1/600/600](https://www.google.com/search?q=https://picsum.photos/seed/p001-1/600/600)",
      "[https://picsum.photos/seed/p001-2/600/600](https://www.google.com/search?q=https://picsum.photos/seed/p001-2/600/600)"
    ],
    "name": {
      "ko": "맥캘란 12년 셰리 오크",
      "en": "Macallan 12Y Sherry Oak",
      "ja": "マッカラン 12年 シェリーオーク"
    },
    "description": {
      "ko": "깊은 과일 향과 스파이스가 어우러진 클래식한 싱글 몰트 위스키입니다.",
      "en": "A classic single malt whiskey with deep fruit notes and subtle spice.",
      "ja": "豊かな果実味とスパイスが調和したクラシックなシングルモルトウイスキーです。"
    }
  },
  {
    "productId": "p002",
    "category": "liquor",
    "subcategory": "gin",
    "brand": "Hendrick's",
    "price": 4800,
    "stock": 12,
    "rateAvg": 0,
    "rateCount": 0,
    "tags": [
      "botanical",
      "cucumber",
      "rose",
      "refreshing"
    ],
    "images": [
      "[https://picsum.photos/seed/p002-1/600/600](https://www.google.com/search?q=https://picsum.photos/seed/p002-1/600/600)",
      "[https://picsum.photos/seed/p002-2/600/600](https://www.google.com/search?q=https://picsum.photos/seed/p002-2/600/600)",
      "[https://picsum.photos/seed/p002-3/600/600](https://www.google.com/search?q=https://picsum.photos/seed/p002-3/600/600)"
    ],
    "name": {
      "ko": "핸드릭스 진",
      "en": "Hendrick's Gin",
      "ja": "ヘンドリックス ジン"
    },
    "description": {
      "ko": "오이와 장미 꽃잎의 독특한 향이 특징인 프리미엄 진입니다.",
      "en": "A premium gin featuring unique notes of cucumber and rose petals.",
      "ja": "キュウリとバラの花びらの独特な香りが特徴のプレミアムジンです。"
    }
  },
  ...

- 이미지 파일은 일단 나중에 처리하기로 했기 때문에 적당한 URL을 집어넣도록 했다.

- ChatGPT는 출력할 문서의 길이가 길어지면 고장이 잘 나서, 프롬프트 작성만 부탁하고 데이터 출력은 Gemini를 통해 했는데, 만족스럽게 잘 출력해주었다.

 

 

Admin SDK로 Firestore에 더미 데이터 입력하기

- 이제 위의 데이터를 Firestore에 넣어줄 차례이다.

- 데이터의 양이 많은 관계로, 시드 스크립트를 만들어서 넣어줄 생각이다. 데이터 시딩을 할 때엔 권한 설정 등을 스킵해버리기 위해, 관리자 권한으로 데이터베이스에 접근해 직접 데이터를 입력하는 방식으로 한다. 절차는 다음과 같다.

 

1. 프로젝트 루트에 seed 폴더를 만들고, 데이터와 스크립트를 넣어준다.

/project-root
  /seed
    products.json   // 데이터
    seedProducts.ts // 스크립트

 

2. 필요한 패키지를 설치해준다.

npm install firebase-admin
npm install -D ts-node

 

3. 프로젝트 루트의 .env.local 파일에 환경변수를 추가해준다.

FIREBASE_PROJECT_ID=xxxxx
FIREBASE_CLIENT_EMAIL=xxxxx
FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nxxxxx\n-----END PRIVATE KEY-----\n"

- 이 코드를 추가해준다. 

- 각 값은, Firebase콘솔 → '프로젝트 설정' → '서비스 계정' → '새 비공개 키 생성' 으로 확인할 수 있다. 

 

4. 시드 스크립트를 작성한다.

import dotenv from "dotenv";
import path from "path";

dotenv.config({ path: path.resolve(process.cwd(), ".env.local") });

import admin from "firebase-admin";
import fs from "fs";

admin.initializeApp({
  credential: admin.credential.cert({
    projectId: process.env.FIREBASE_PROJECT_ID,
    clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
    privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, "\n"),
  }),
});

const db = admin.firestore();

const filePath = path.join(process.cwd(), "seed", "products.json");
const products = JSON.parse(fs.readFileSync(filePath, "utf8"));

async function seedProducts() {
  const batch = db.batch();

  products.forEach((product: any) => {
    const docRef = db.collection("products").doc(product.productId);
    batch.set(docRef, product);
  });

  await batch.commit();
  console.log("✅ Products successfully seeded!");
}

seedProducts().catch(console.error);

 

5. 커맨드를 실행해준다.

npx ts-node seed/seedProducts.ts

 

6. Firestore로 들어가서 데이터가 잘 들어갔는지 확인한다.