Skip to content

HoYunBros/best-robbins-fe

Folders and files

NameName
Last commit message
Last commit date

Latest commit

f75b2e0 · Oct 16, 2023

History

74 Commits
Oct 5, 2023
Oct 11, 2023
Oct 13, 2023
Oct 5, 2023
Oct 5, 2023
Oct 5, 2023
Oct 5, 2023
Oct 5, 2023
Oct 16, 2023
Oct 12, 2023
Oct 11, 2023
Oct 11, 2023
Oct 5, 2023
Oct 10, 2023
Oct 5, 2023

Repository files navigation

🔨 프로젝트 소개

🍦 베스트 라빈스(Convert to Next.js)

선택하기 어려운 수많은 아이스크림 조합들, 우리가 추천해줄게!

배스킨 라빈스 아이스크림을 모르거나 조합을 결정하기 힘든 사람들을 위한 재료 기반 아이스크림 추천 서비스

🔨 프로젝트 배포 혹은 데모

배포: 🍦 베스트라빈스

🔨 프로젝트 주요 기능

  • 메인 페이지의 캐로셀 슬라이더를 통해 독특한 아이스크림 조합을 추천받을 수 있습니다.
    • 데스크탑 및 모바일 환경에서 모두 슬라이드를 사용할 수 있습니다.
  • 라이트 모드와 다크 모드를 지원합니다.
  • 원하는 사이즈와 재료들을 선택하여 아이스크림 조합을 추천받을 수 있습니다.

구현 예정 기능(우선 순위 별)

  • Oauth 로그인(카카오 예정)
  • 유저 별 조합 북마크 기능
  • 온보딩 페이지(PWA를 활용할 수 있는 방법을 안내하는 페이지)
  • 주위 배스킨라빈스 위치 제공 페이지(지도 API 사용 예정)
  • 아이스크림 조합 공유 기능(링크 공유 및 SNS 공유)

🔨 프로젝트 구조 및 기술 스택

1. 프로젝트 구조

📦 
├─ .eslintignore
├─ .eslintrc.json
├─ .gitignore
├─ .husky
├─ .prettierignore
├─ .prettierrc.json
├─ README.md
├─ next.config.js
├─ package-lock.json
├─ package.json
├─ postcss.config.js
├─ public
│  ├─ fonts
│  ├─ icons
│  ├─ images
│  ├─ manifest.json
├─ src
│  ├─ app
│  │  ├─ (combination)
│  │  │  ├─ ingredient
│  │  │  ├─ result
│  │  │  │  └─ [slug]
│  │  │  └─ size
│  │  ├─ user
│  │  ├─ favicon.ico
│  │  ├─ globals.css
│  │  ├─ layout.tsx
│  │  ├─ not-found.tsx
│  │  ├─ page.tsx
│  │  ├─ robots.ts
│  │  ├─ sitemap.ts
│  │  └─ theme-provider.tsx
│  ├─ components
│  │  ├─ common
│  │  │  ├─ BackButton
│  │  │  ├─ CloseButton
│  │  │  ├─ GlobalNavBar
│  │  │  ├─ RecipeCard
│  │  │  └─ TopNavBar
│  │  ├─ home
│  │  │  ├─ Carousel
│  │  │  ├─ Logo
│  │  │  └─ ThemeSwitcher
│  │  ├─ ingredient
│  │  │  ├─ IngredientFooter
│  │  │  └─ IngredientsBoard
│  │  ├─ result
│  │  │  └─ ResultFooter
│  │  └─ size
│  │     ├─ SizeFooter
│  │     └─ SizeRow
│  ├─ constants
│  ├─ services
│  │  ├─ getIngredients
│  │  ├─ getRecipe
│  │  ├─ getRecommendations
│  │  ├─ getSizes
│  │  ├─ http
│  │  └─ postRecipe
│  ├─ stores
│  │  └─ useUserSelectStore
│  ├─ types
│  └─ utils
├─ tailwind.config.ts
└─ tsconfig.json

©generated by Project Tree Generator

2. 주요 기술 스택

목적 이름 버전
언어 TypeScript ^5
UI React ^18
UI Next 13.5.4
스타일 tailwindcss ^3
상태관리 zustand ^4.4.3
환경 설정 prettier ^3.0.3
환경 설정 eslint ^8
환경 설정 husky ^8.0.0
환경 설정 lint-staged ^14.0.1

🔨 프로젝트 특이사항

1. SEO(Search Engine Optimization) 최적화

1. 클라이언트 / 서버 컴포넌트 구분

기존 React로만 구현했던 프로젝트에서는 앱 자체가 CSR로 동작하기에 SEO에 취약하다는 단점이 있었습니다. 이번 프로젝트에서는 Next.js 13에서 지원하는 클라이언트, 서버 컴포넌트를 통해 SEO를 최적화하였습니다. 프로젝트를 시작하기 전 컴포넌트 구분 기준을 아래와 같이 구분해보았습니다.

  1. 클라이언트 컴포넌트: 유저 인터렉션이 필요한 경우
  2. 서버 컴포넌트: api 통신이 필요한 경우
    • SSG: 정적인 데이터만을 사용하는 경우
    • SSR: 동적인 데이터를 사용하는 경우

Next.js에서 기본적으로 SSG 렌더링을 권장하는 만큼, 최대한 SSG로 구현을 하면서 필요에 따라 CSR 혹은 SSR을 적용하였습니다.

Server/Client Component 구분

2. robots.ts 및 sitemap.ts 생성

검색 엔진이 사이트를 크롤링할 때 참고하는 파일인 robots.txt와 검색 엔진에게 사이트의 구조를 알려주는 파일인 sitemap.xml을 제공하여 SEO를 최적화하였습니다.

Next.js의 robots.tssitemap.ts를 통해 typescript 코드를 이용하여 비교적 간단하게 위 두 파일을 생성할 수 있었습니다.

robots.ts

import { MetadataRoute } from 'next';

export default function robots(): MetadataRoute.Robots {
  return {
    rules: {
      userAgent: '*',
      allow: '/',
    },
    sitemap: 'https://best-robbins-fe.vercel.app/sitemap.xml',
  };
}

sitemap.ts

import { MetadataRoute } from 'next';

export default function sitemap(): MetadataRoute.Sitemap {
  return [
    {
      url: 'https://best-robbins-fe.vercel.app/',
      lastModified: new Date(),
      changeFrequency: 'yearly',
      priority: 1,
    },
    {
      url: 'https://best-robbins-fe.vercel.app/size',
      lastModified: new Date(),
      changeFrequency: 'yearly',
      priority: 0.8,
    },
    {
      url: 'https://best-robbins-fe.vercel.app/ingredient',
      lastModified: new Date(),
      changeFrequency: 'monthly',
      priority: 0.5,
    },
  ];
}

2. zustand를 통한 상태 관리

기존 React 프로젝트에서는 Context API와 useState를 사용하여 유저의 선택 상태를 전역으로 관리하였습니다. 이 때, 상태 Provider를 페이지 별 최상단에 위치하다보니 유저의 선택이 변경될 때마다 상태와 관련없는 컴포넌트까지 리렌더링이 일어나게 되었습니다. 이를 해결하고자 zustand 라이브러리를 사용하여 유저의 선택 상태에 대한 store를 만들고 useStore를 통해 필요한 컴포넌트에서만 상태를 사용하도록 함으로써 불필요한 리렌더링을 방지할 수 있었습니다.

useUserSelectStore.ts

import { create } from 'zustand';

import { UserSelect } from '@/types';

type State = {
  userSelect: UserSelect;
};

type Action = {
  setUserSelectSizeId: (sizeId: number) => void;
  setUserSelectSizeValue: (sizeValue: number) => void;
  setUserSelectIngredientIds: (ingredientIds: number[]) => void;
  reset: () => void;
};

const initialState: State = {
  userSelect: {
    sizeId: 0,
    sizeValue: 0,
    ingredientIds: [],
  },
};

export const useUserSelectStore = create<State & Action>()(set => ({
  ...initialState,
  setUserSelectSizeId: (sizeId: number) =>
    set(state => ({ userSelect: { ...state.userSelect, sizeId } })),
  setUserSelectSizeValue: (sizeValue: number) =>
    set(state => ({ userSelect: { ...state.userSelect, sizeValue } })),
  setUserSelectIngredientIds: (ingredientIds: number[]) =>
    set(state => ({ userSelect: { ...state.userSelect, ingredientIds } })),
  reset: () => set(initialState),
}));

3. 서버 통신 횡단 관심사 분리

React 프로젝트 때는 axios의 Instance 기능을 사용하여 api 통신을 다루는 인스턴스를 만들고 해당 인스턴스를 여러 service 코드에서 react query와 함께 사용함으로써 횡단 관심사를 분리할 수 있었습니다.

하지만 Next를 사용할 때는 기본적으로 제공하는 fetch api가 기본적으로 캐싱 기능을 지원해주었고 데이터를 fetch 하는 부분은 서버 컴포넌트에서 다루기에 axios와 react query를 사용하지 않았습니다.

따라서 httpClient라는 Class 객체를 만들고 해당 객체를 통해 api 통신을 다루는 인스턴스를 만들어 횡단 관심사를 분리하였습니다.(추후 로그인 관련 token 로직을 추가할 때 유용할 것 같아서 Class 객체로 만들었습니다.)

httpClient.ts

export class HttpClient {
  private baseUrl: string;
  constructor(baseUrl: string) {
    this.baseUrl = baseUrl;
  }

  fetch(path: string, options = {}) {
    return fetch(`${this.baseUrl}${path}`, {
      ...options,
      headers: {
        'Content-Type': 'application/json',
      },
    });
  }
}

service 코드 예시: getRecipe.ts

import { HttpClient } from '../http';

import { Recipe } from '@/types';

export async function getRecipe(recipeId: number): Promise<Recipe> {
  if (!process.env.NEXT_PUBLIC_BASE_API_URL) throw new Error('존재하지 않는 환경변수입니다.');

  const httpClient = new HttpClient(process.env.NEXT_PUBLIC_BASE_API_URL);
  const response = await httpClient.fetch(`/recipes/${recipeId}`);
  const json = await response.json();
  return json.body;
}

🔨 프로젝트 회고

Next.js로 마이그레이션 후

왜 React는 라이브러리이고 Next는 프레임워크인지 직접적으로 체감할 수 있는 시간이었습니다. 처음에는 React보다 더 엄격한 구조에 불편함을 느끼기도 했지만, 다양한 규칙과 api 속에서 좀더 통일된 코드를 작성할 수 있었고 React만을 사용할 때는 큰 고민을 하지 못했던 Rendering 방식, 이미지 최적화, SEO 등에 대해서 고민해보고 공부할 수 있었습니다. 이 모든 것들이 결국엔 React에서 제공하는 기능들을 추상화한 것이고 그 React마저 JavaScript의 라이브러리이기 때문에 JavaScript를 공부하고 익히는 것이 중요하다는 것을 다시 한번 느낄 수 있었습니다.

🔨 만든이

Profile Contact
이메일: jaydenlee.dev@gmail.com
이력서: Jayden's Resume
블로그: Jayden {do: smite}

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published