본문 바로가기

Business Ops

Local_TripTags

여행 플랫폼의 MVP버전이다. Javascript로 만들어서 AWS의 Lambda를 이용해 저비용으로도 구동할수 있도록 가벼운 형태로 개발해 보았다. NestJS의 모노레포의 포트폴리오용으로 프론트는 NextJS로 SEO를 구현하고, Backend는 NestJS의 Monorepo로 해서, Lambda로 구동할수 있도록 가볍게 구성했다.

 

계기는 최근 구독하는 유투브에서 스위스의 짚올린 헛간에서 숙박하는 (알프스의 소녀 하이디처럼) 콘텐츠를 접하고부터이다. 예전부터 세계의 여러 사람들이 서로의 문화를 공유하고 나누는 것을 상상해 왔고, 예전에 마이언펠트의 농가에서 헛간체험을 한 적도 있었다. 그런 내용이 콘텐츠나 플랫폼이 되기에는 당시에는 통신이며 기반 기설이 약할 때여서, 당시 네이버 밴드에 올린 동영상 몇개와 사진 몇점만 남아있지만, 스위스에서 농가를 숙소로서 개방하는 프로그램을 운영하는 것을 찾아서 예약을 하고 ( 여름에만 가능 ) 교통이 좋지 않은 탓에 식사와 픽업을 받고 근처 온천에도 들렀었다.

잊을 수 없는 추억이지만 현지 컨택 과정이 쉽지 않았다. 그래서, 전 세계 도시별로 찐 로컬 주민들의 후기와 협조를 모아서 여행을 차별화해보면 어떨까? Guide를 주면 어떨까, 세상 사람들이 서로 교류하면 어떨까 .. 하는 기획을 해보았다. 서로 다른 도시간에 로컬 간 교류는 쉽지 않다. 방문객도 호스트도 믿을수 있어야 하고, 운영이 되려면 사업화 요소가 있어야 하니 여러가지 고려해서 3단계까지 확장하는 안을 구성해봤다.  

1. 기본 구성

1) 사용자 권한 

🥉 Bronze (신규) - 최소 요건: 거주 증명 - 콘텐츠 5개 이상 - 후기 10개 이상

🥈 Silver (검증) - 거주 6개월 이상 증명 - 콘텐츠 20개 이상 - 평점 4.5 이상 - 후기 50개 이상

🥇 Gold (전문가) - 거주 2년 이상 증명 - 콘텐츠 50개 이상 - 평점 4.7 이상 - 후기 200개 이상

💎 Diamond (VIP) - 거주 5년 이상 - 콘텐츠 100개 이상 - 평점 4.9 이상 

 

2) 혼자 여행 Guide

✅ 안전 정보 - 안전한 지역 / 피해야 할 지역 - 야간 이동 팁 - 긴급 연락처

✅ 혼밥 맛집 - 바 좌석 있는 곳 - 혼자 가도 어색하지 않은 곳 - 빠른 식사 가능한 곳

✅ 혼자 즐길 수 있는 활동 - 박물관, 미술관 - 산책 코스 - 카페

✅ 혼자 묵기 좋은 숙소 - 게스트하우스 - 호스텔 (여성 전용 등) - 안전한 동네

 

3) 동행 찾기 

- 같은 날짜, 같은 도시 여행하는 사람 찾기

- 안전장치: ✅ 신원 인증 필수 ✅ 공개 프로필 ✅ 여성 전용 옵션 ✅ 신고 시스템

예: "12월 20-25일 로마 30대 여성 혼자 여행 미술관/맛집 관심 함께 식사할 분 찾아요"

 

4) 각종 정보

(1) 교통 수단 : 기차, 지하철, 버스, 택시 

(2) 예산별 여행 Route추천 혹은 가이드 

저예산 (1일 €50): - 숙소: 호스텔 €20 - 식사: 피자+파스타 €20 - 교통: €5 - 입장료: €5

중간 (1일 €100): - 숙소: 호텔 €60 - 식사: 레스토랑 €30 - 교통: €10

**조언 팁: - 현금 vs 카드 - 팁 문화 (의무 아님) - ATM 수수료 피하기

 

2. 수익모델 

1) 구독료

무료 (12개월) - 사용자 확보, 콘텐츠 축적, 신뢰구축 

** 상세 가이드(pdf 다운로드) 광고X, 여행 가이드 작성 및 수익활동 가능

 

2) B2B 도입

- 제휴 ( 여행사 , 기업 출장 가이드, 교환 프로그램 )

- 개인 가이드 프로그램 ( 법적인 제한이 없는 한도내에서 개인 라이드 및 예약 수수료)

 

3.  시스템 구성

1) Frontend

 - Next.js (SEO), React Native or Flutter(앱), 다국어 (i18n), 지도 (Google Maps API)

2) Backend 

 - NestJS , PostgreSQL , PostGIS , Redis

3) Infra

 - AWS EC2, S3, CloudFront, Lambda

** 추가 검토사항 : 번역 API (자동 번역) , 국가별 지도 API,  결제 (글로벌), 알림 

 

4. 주요 기능 설계 및 확장 Plan 

 

1) Phase 1: MVP 

- 공공 데이터 기반 관광 정보를 기반으로 자율적인 리뷰에 기반한 Rating시스템 

- 로컬과 일반 사용자로 리뷰 구분 가능 

 

  (1) 인증 시스템

     - 회원가입 (이메일/소셜)

     - 로컬 거주지 인증

     - 프로필 (자기소개, 사진, 관심사)

 

 (2) 장소 시스템 (국내 우선)

     - 장소 등록 (이름, 주소, 카테고리, 사진)

     - 리뷰 작성 (별점, 후기, 팁)

     - 지역별/카테고리별 필터링

     - 지도 연동

 

(3) 마이 페이지 

     - 내 정보 관리 

     - 등록된 Place와 리뷰 조회 및 관리 

     - 나의 관심 지역 등록 

 

  2) Phase 2: 소셜 & 교류

 

  (1) 커뮤니티

     - 질문 게시판 (도시별) 

     - 답변 시스템

     - 좋아요/북마크

 

 (2) 포인트 시스템

     - 리뷰 : 좋아요와 도움이 되었어요 갯수에 따라 포인트 추가

 

(3) 매칭 시스템

     - "여행 친구 찾기"

     - 로컬-여행자 매칭

     - 관심사 기반 매칭

     - 일정 조율

 

(4) 메시징 및 알림 

     - 1:1 채팅 및 그룹 채팅

     - 일정 공유

 

(5) 자체 Rating ( 배지 & 랭킹 )

    - Top Contributor (도시별)

    - Expert (카테고리별)

    - Friendly Local

 

3) Phase 3: 사업화 

 

(1) 로컬 수익 서비스 (개인)

     - 개인별 가이드 작성 기능

     - 로컬 검증 및 예약 시스템 

     - 예약 시스템

     - 리뷰 & 평점

 

  (2) 여행 기획 서비스

     - 맞춤 루트 제작

     - 일정 공유

    - PDF 다운로드

 

 (3) Premium 기능

    - B2B 도입 

 

5. MVP 구현 범위 설정

- 현지인이 알려주는 지역정보 및 Place Review, TripTags Korea

- 국내 기준 지역 관광지/음식점/숙박업소 리뷰 작성/조회

 

1) 구현 내용  

(1) Place기본 정보 DB 구성 및 다국어 구성

- data.go.kr의 공공 api로 국내 관광 관련 정보 기본 db에 설정  

- 다국어 Menu지원 (8개 국어)

 

(2) 사용자 Place등록 기능 

- 음식점, 카페, 숙박업소, 관광지, 쇼핑, 자연, 문화 Place등록 

- 구글 Map Marker를 이용한 Place등록 

- 검색기능을 이용한 등록 ( 구글 및 KaKao Map API를 이용한 등록 )

 

(3) Place에 대한 평가 기능 

-  등록된 Place에 대한 평가 기능 ( 리뷰 및 평점 추가등록 )

- 일반 사용자, 로컬로 구분하여 표시 ( 위치 인증을 통한 local 구분 )

 

(4) MyPage 기능 

- 관심 여행지 등록 후 관련 Review 조회 

 

2) 기술 Stack 및 논리 DB 

(1) 기술 스택

① NextJS(프론트 엔드), NestJs(벡엔드) 

- 규모가 비교적 작고, Cloud활용시 Serverless적용에 적합한 Node기반

② 패키지 관리자 pnpm 적용

 - 모노레포에 특히 유리 ( Typescript, dotenv, lodash, @types등을 공유 )

 

③ DB 

- Redis, Postegres DB 도커로 구동 

④ 인증 및 외부 API

- 자체 로그인 및 Oauth (Google, Naver, Kakao)

- 구글 Map API 및 Kakao Map API, 다국어 (i18n) 

 

(2) 주요 모듈 및 비지니스 로직  

- Auth, Users, Venues, Reviews

① Users

- user, userDetails, localVerification, businessVerification,  userPoints 테이블 구성 

- 사용자 탈퇴는 소프트 삭제처리 (  관련 정보 익명처리 )

  • 좋아요 삭제 ( ReviewLike 테이블 userId 레코드 삭제 )
  • 리뷰 익명화: Review 테이블 ( userId NULL 업데이트 )
  • 평점 재계산: VenueStats 테이블 ( venue의 rating, review_count 재계산 )

Venues

- venue, venueDetail, googlePlace, KoreanLocal(한국관광정보 API), image 테이블 구성

- review, reviewHelpful 테이블 연동 

- venue card의 필수 정보 중복저장 ( 역정규화 : thumbmail, review_count, overall_rating ) 

 

③ Reviews 

- review, reviewDetails, reviewHelpful 테이블 구성

- venus, users 연동 

 

(3)  ERD  

ERD초안 (https://dbdiagram.io/)

 

6. MVP 기본구성 

1) 모노레포 기본 및 DB Infra 

(1) 모노레포 구성 

① 루트 폴더 (기본 구성 설정)

- 폴더 생성 및 pnpm 설치

- workspace설정 (pnpm-workspace.yaml, 개별 패키지 모듈인식 , 심볼릭 링크로 참조)

packages:
  - "apps/*"
  - "packages/*"

catalogs:
  prisma:
    prisma: "6.4.1"
    "@prisma/client": "6.4.1"

 

기본 gitignore, env, readme등의 기본 구성

- DB와 Redis구동 컨테이너 설정 (docker-compose.yml)

tsconfig.json에 공용 모듈정보 paths로 추가 
 "paths": {
    "@/*" : ["src/*"],
    "@triptags/shared": ["../../packages/shared/src"] 
}

 

공용 모듈 설정 ( Packages/shared )

- TypeScript관련 기본 구성이나 공통 함수를 정의

- 빌드 혹은 컴파일 시에 프론트와 백엔드에 관련 리소스를 제공

 

(2) 프레임워크와 의존성 설치 ( 프론트와 백엔드 기본구성 ) 

 프론트엔드 - NextJS 설치 및 의존성 적용

//프론트엔드 (NextJs)
npx create-next-app web --ts --eslint --src-dir --use-pnpm
pnpm add @react-google-maps/api @tanstack/react-query @vis.gl/react-google-maps axios next-intl zustand

 

 백엔드 - NestJS 설치 및 의존성 적용 

//백엔드 (NestJS)
npx @nestjs/cli new api --package-manager=pnpm --skip-install
pnpm add @nestjs/config @nestjs/jwt @nestjs/passport @nestjs/throttler @prisma/client bcryptjs passport passport-jwt passport-google-oauth20 passport-kakao passport-naver-v2 cookie-parser class-validator  class-transformer
 @nestjs/cache-manager cache-manager-ioredis-yet ioredis 

//의존성 및 typescript설정에 공용 모듈추가 
// package.json dependency에 정보추가  
"@triptags/shared": "workspace:*",
    
//tsconfig.json에 공용 모듈정보 paths로 추가 
 "paths": {
    "@/*" : ["src/*"],
    "@triptags/shared": ["../../packages/shared/src"] 
}

 

 

(3) PRISMA 구성 (ORM) 

① PRISMA Schema 구성 

- schema.prisma에 model 및 relation 서술

 

② PRISMA MIGRATION생성 

- DB에 적용할 sql문을 생성하여 schema생성 

- prisma studio로 구성확인 

//환경변수를 이용하여 prisma migrate (init sql생성, local에서 수행)
pnpm exec dotenv -e ../../.env -e .env -- pnpm exec prisma generate //client 
pnpm exec dotenv -e .env.local -- pnpm exec prisma migrate dev --name init 

//studio실행
pnpm exec dotenv -e ../../.env -e .env -- pnpm exec prisma studio

 

③ 국내 기본 데이터 설정 ( 공공 데이터 포털에서 국내 관광 관련 venue의 기본자료 받아오기 )

//관련 URL구성 
BASE_URL = 'https://apis.data.go.kr/B551011/KorService2'; 
const url = `${BASE_URL}/areaBasedList2?serviceKey=${TOUR_API_KEY}&${params.toString()}`;

//seed-tourism.ts구동
pnpm exec dotenv -e ../../.env -e .env -- pnpm exec ts-node apps/api/common/seed-tourism.ts

공공 데이터 API Contents, 행정구역 (지역, 시/구, 동(도로명), 세부주소)를 Hierarchy를 가지도록 구성함

 

Prisma Studio를 이용한 schema확인 및 공공 데이터 생성결과

 

(4) REDIS를 비롯한 라이브러리 구성

① REDIS의 활용  

- Token 저장 및 Cache

- controller, service 구성

 

② Swagger생성 

- Nest Cli에 관련 옵션 구성

//nest_cli.json에 swagger관련 설정
    "plugins": [
      {
        "name": "@nestjs/swagger",
        "options": {
          "classValidatorShim": true, //데코레이터 자동인식
          "introspectComments": true  //주석 읽기
        }
      }

 

 

2) BackEnd구성

(1) API구성 

① API 명세 ( Notion활용 )

 

 

② Swagger적용

Swagger로 사용자 생성 ( Prisma Studio에서 생성확인)

 

 

(2) Module 구성 및 API 

① 사용자와 명소  

- User, UserDetail, Region, Venue, VenueDetail

 

② 사용자 Point 및 Local(거주민)인증 

- UserPoint, LocalVerification을 통해 정보 허용범위를 결정하는 point적립

- NestJS의 eventEmitter2 모듈을 적용하여 비동기 처리 

 

  행정 구역정보  

- Region ( 국가, 시/도, 시/군/구)

- 국가, 시/도, 하위시/군/구 3단계로 구분, 각 record는 parentId로 내부에서 hierarchy구현

- 국가 코드는 i18n-iso-countries라이브러리 처리 (ISO 3166 국가 코드 기준)

 

④ Review, ReviewDetail, Helpful(도움돼요)등에 Point 부여 기능처리   

- 사용자 후기, 평점, 후기에 대한 평가 

 

⑤ 기타 모듈

- Tag(태그), Destination(선호여행지), venueStats(통계)

 

⑥ 공통 Type구현 (Shared)

- 각 모듈의 주요 입력 Type을 DTO형식으로 구현 ( Front와 공유 )

 

3) FrontEnd구성

(1) 다국어 설정 (next intl & middleware사용 )

  다국어 설정을 위한 기본 구조 (Locale의 규칙과 연동 방식 설정)

- 언어설정 규칙을 Routing으로 구성 ( url경로에 추가하게 설정 ) 

- nextConfig를 언어 경로로서 감싸줌 (createNextIntlPlugin에  Routing을 추가)

 

② 언어 자동감지 middeware (직접적인 경로 설정 담당)

- 다국어 설정에 맞게 route를 추가 ( 다국어 기본 설정에 언어 옵션을 반영하거나 default 연결 )

//route기본 설정
export const routing = defineRouting({
  locales: ["ko", "en", "ja", "zh", "es", "fr", "de"],
  defaultLocale: "ko",
  localePrefix: "always",
})
export const { Link, redirect, usePathname, useRouter } = createNavigation(routing)

//getRequestConfig설정
export default getRequestConfig(async ({ requestLocale }) => {
  let locale = await requestLocale

  if (!locale || !routing.locales.includes(locale as any)) {
    locale = routing.defaultLocale
 
   return {
    locale,
    messages: (await import(`../messages/${locale}.json`)).default,
  }  

//nextConfig처리 
import createNextIntlPlugin from "next-intl/plugin"
const nextConfig = {
}
const withNextIntl = createNextIntlPlugin("./src/i18n/request.ts")
export default withNextIntl(nextConfig)

//Layout에서 children감싸기
<body>
  <NextIntlClientProvider messages={messages}>
   {children}
  </NextIntlClientProvider>
</body>

 

③ Message (언어별 설정 파일)

- 화면의 기본 설정에 사용되는 용어들을 언어 버전의 json으로 구성 

//ko.json
{
  "HomePage": {
    "title": "TripTags - 진짜 여행, 현지의 목소리",
    "description": "현지인과 여행자의 경험을 구분하여 진정한 여행 정보를 제공합니다",
    "exploreVenues": "장소 둘러보기",
    "getStarted": "시작하기",
    "localSection": {
      "title": "로컬의 추천",
      "description": "그 지역에 실제로 살고 있는 주민들의 진짜 추천"
    },
    "travelerSection": {
      "title": "여행자의 경험",
      "description": "여행자 관점에서 본 실제 방문 후기"
    }
  }
}

 

④ Layout에 NextIntlClientProvider로 Locale반영 

- URL에 포함된 언어 설정 반영 ( useTranslations Hook을 적용 )

- 언어 설정에 따라 message에 포함된 하위 객체의 내용을 반영하여 렌더링 

 

(2) API설정 

 타입 설정

- Shared의 공통구성에서 DB의 Model을 import하여 사용 

 

 

 

** 실행 화면 

동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.

 

 

_______________________________________________________________________

 

** 모노레포에서 pnpm의 역할

   -  시스템 레벨에서 패키지 연결, require / import 시 파일 찾기

   - 공통 양식을 저장하나 TypeScript 컴파일 시 타입 검색이나 IDE의 타입 인텔리센스 적용 불가, 별도로 Typescript관련 설정 필요  

 

** Prisma에 구동 시 Call Stack오류발생 

schema.prisma파일 내부의 재귀나 상호참조에서 전혀 문제가 없고 format을 통한 문법체크에서도 아무 문제가 없이 통과되었으 prisma generate와 migrate에서 알수 없는 CallStack오류 발생.

 

1) 오류

   - 모노레포에서 prisma generate와 migraion에서 callStack이 소진됨 

   - Error: Maximum call stack size exceeded

2) 원인 추정

   - Prisma가 모노레포의 백엔드와 루트의 환경변수(.env)참조를 위한 탐색 실패 

   - pnpm의 심볼릭 링크체계에서 무한루프 발생추정( 환경변수 조합 reduce실패 ) 

   - 로그 : at Array.reduce (<anonymous>)
                at n (/Users/elena/Desktop/.../prisma/build/index.js:145:4352)                  

3) 해결

  - 백엔드의 환경변수 위치를 dotenv -e로 규정해줌 (prisma가 자동 파싱 하지 않음)

  -  dotenv -e 명령어를 통해 백엔드와 루트의 .env의 위치를 알려줌 

  -  명령어 : pnpm exec dotenv -e ../../.env -e .env -- pnpm exec prisma generate

4) 느낀점

AI의 조언을 듣고 구조적인 수정이나 버전 변경을 수행할때는 매우 주의 필요. 구조의 일부를 수정하면 점입가경이 되어 점점 더 많은 작업을 하다가 원상복구하게 된다.  (물론, 이를 해결하며 성장은 있겠지만..)결국, 개별 검색을 통해 표준 문서를 참조한 사례를 찾아 코드 간소화 및 디버그 실행하여 문제해결함. 

- schema.prisma내의 변수 설정 및 상호 참조와 관련없는 부분까지 수정한 뒤 전부 원상 복귀

 - node_modules나 각종 설정을 재설치하고 cache 삭제수행/output을 별도 지정/binary로 클라이언트의 옵션을 주었어도 해결되지 않으니 원인을 찾지 못하는 문제라며 node의 버전을 LTS(짝수버전)으로 바꾸라 했다가 22라고 하니깐 20으로 다운하라는 엉뚱한 조언을 하거나, 상호참조 혹은 구조적 문제를 언급하며 전체를 들어엎으라고 권장했음

 

** PNPM 모노레포 적용시 PrismaClient의 심볼릭 링크의 문제 

1) 오류

- DB schema변경후 User model을 가져오지 못하고 오류 발생 

2) 원인 추정

 - DB schema변경후 pnpm의 @triptags/prisma의 심볼릭이 실제의 위치를 찾지못함

3) 해결

- 안전한 구조를 위해 Prisma 표준 문서에 따라 catalogs적용 (pnpm사용시 symbolic link와 실제 위치의 혼선방지)

- pnpm 버전 조정 후 키 보안 키 에러(Corepack)처리를 위한 전역업데이트 수행 (9.9.5 이상 필수)

- db에서 PrismaClient를 Singleton으로 구성하여 안정성 확보

// 루트의 workspace설정 
packages:
  - "apps/*"
  - "packages/*"

catalogs:
  prisma:
    prisma: "6.4.1"
    "@prisma/client": "6.4.1"
    
    
//pnpm버전 버전 업 및 전역 업데이트
npm install -g pnpm@9.15.9

4) 느낀점

패키지 매니저로서 장점을 고려하여 pnpm을 도입하였으나, 모노레포와 심볼릭 링크를 잘 사용하려면 좀 더 심도있는 이해가 필요하다. 

 

** TS 증분 컴파일과 관련 빌드 Problem Solving 

모노레포 설정으로 백엔드, 프론트엔드가 Shared(공통 타입과 Util설정)폴더의 내용을 공유하고 있는 상태로 Swagger를 설정하여 로직을 검증하려 하였으나 빌드와 실행에 계속 실패함. 모든 검증 로직과 라이브러리가 로그에 정확하게 나타나며 이상이 없는데도 main파일을 찾지 못한다는 오류가 나며 dist폴더가 생기지 않음.  

 

1) 오류 

- 공유 모듈(packages/shared)에서 빌드가 되지 않거나 빌드 결과물이 소스와 공존

- 백엔드의 타입 import에서 지속적으로 오류가 나타나며 빌드에 실패

   로그에서 라이브러리를 읽어오는데 이상이 없으나 컴파일할 소스파일에 접근하지 않음 

2) 원인 추정

   - 백엔드에서 Shared폴더Typescript 컴파일 결과물을 인식 실패 (1차 오류 발생)

   - tsconfig.build.json에서 모든 빌드가 끝난것으로 인식 (2차 오류)

      캐시(tsbuildinfo)가 적절히 구성되지 못해 dist emit이 발생하지 않음  

      TypeScript의 증분 컴파일 문제 

3) 해결

   - Shared폴더에서 tsconfig.json에 CJS모듈과 EMS모듈용 옵션을 별도로 구성 

      백엔드에서는 CJS모듈에 연결하도록 구현, rootDir, outDir 조건 재설정 

   -  tsconfig.build.json에서 컴파일 옵션에 "incremental": false 추가

4) 느낀점

파일을 모두 출력해서 관련 라이브러리에 접근은 되며, 엔진이 소스에 컴파일 할것이 없다고 판단

pnpm exec tsc -p tsconfig.build.json --showConfig로 조건중에 "incremental": true를 상속한 상태임을 확인하여 우선 이를 false로 줌, nest-cli.json이나 tsconfig.json의 설정을 수정했는데 왜 변경사항을 인식하지 못했는지 모르겠다. 모노레포와 shared의 복잡한 폴더 구성때문인지 symbolic link를 사용하는 가상경로 때문에 어디선가 해석에 무리가 있는건지도 모르겠다.

TS가 증분 컴파일을 한다고 이론적으로는 알고 있으나 실제 코드 구동중에는 미처 생각하지 못하고 오래 해맸다.  

 

 

       

 

 

포트폴리오용 사용할 겸 프로젝트로 구현해 보았고, AI를 많이 활용해서 아직도 세부적으로는 공부 중이다.

예전에는 코드를 익힌다는 명목하에 IDE의 자동 완성도 사용하지 않았는데..  

'Business Ops' 카테고리의 다른 글

ByTogether  (0) 2025.11.29
반도체의 유리기판 ?  (0) 2025.02.15