/nemo

Primary LanguageTypeScript

πŸ“ 폴더 ꡬ쑰

β”œβ”€β”€ ...
β”œβ”€β”€ public
β”œβ”€β”€ src
β”‚ β”œβ”€β”€ app
β”‚ β”‚ β”œβ”€β”€ (form)
β”‚ β”‚ β”‚ β”œβ”€β”€ arrival               # 도착지 선택 라우트
β”‚ β”‚ β”‚ β”œβ”€β”€ departure             # μΆœλ°œμ§€ 선택 라우트
β”‚ β”‚ β”‚ β”œβ”€β”€ form                  # 일정 등둝 라우트
β”‚ β”‚ β”‚ └── layout.tsx            # (form) 곡톡 λ ˆμ΄μ•„μ›ƒ
β”‚ β”‚ β”œβ”€β”€ (service)
β”‚ β”‚ β”‚ β”œβ”€β”€ home                  # ν™ˆ 라우트
β”‚ β”‚ β”‚ β”œβ”€β”€ ui                    # home λΌμš°νŠΈμ—μ„œ μ‚¬μš©ν•˜λŠ” μ»΄ν¬λ„ŒνŠΈ λͺ¨μŒ
β”‚ β”‚ β”‚ └── layout.tsx
β”‚ β”‚ └── layout.tsx              # 루트 λ ˆμ΄μ•„μ›ƒ
β”‚ β”œβ”€β”€ components                # κΈ°λ³Έ μ»΄ν¬λ„ŒνŠΈ
β”‚ β”œβ”€β”€ styles
β”‚ β”œβ”€β”€ types                     # 곡톡 νƒ€μž…
β”‚ └── utils
β”‚ β”‚ β”œβ”€β”€ date.ts                 # λ‚ μ§œ, μ‹œκ°„ κ΄€λ ¨ μœ ν‹Έ ν•¨μˆ˜
β”‚ β”‚ └── swrFetcher.ts           # SWR, data fetchers
β”œβ”€β”€ tailwind.config.js
β”œβ”€β”€ next.config.js
β”œβ”€β”€ README.md
└── ...

πŸš€ 개발 μ„œλ²„ μ‹€ν–‰

λΉŒλ“œ 및 μ‹€ν–‰

// 쒅속성 μ„€μΉ˜
yarn install λ˜λŠ” yarn

// 개발 μ„œλ²„ μ‹€ν–‰
yarn dev

μ£Όμš” μ‚¬μš© 기술

Next.js 13, App Router

  • Layouts(Root, Nesting) : 루트 λ ˆμ΄μ•„μ›ƒ 및 쀑첩 λ ˆμ΄μ•„μ›ƒμ„ μ‚¬μš©ν•˜μ—¬ 곡톡 헀더, ν•˜λ‹¨ κ³ μ • λ²„νŠΌ 등을 배치
  • Route Groups : 라우트 그룹을 톡해 곡톡 λ ˆμ΄μ•„μ›ƒ 적용
  • RSC : μ„œλ²„ μ»΄ν¬λ„ŒνŠΈ, μ„œλ²„λ‹¨μ—μ„œ 데이터 패칭 둜직 κ΅¬ν˜„

Data Fetching On the server, with fetch

...

export default async function Page() {
  const record = await getSchedulesMap();
  const recentSchedule = await getRecentSchedule();
  return (
    <Suspense
      fallback={
        <div className='w-full flex justify-center items-center'>
          <Spinner />
        </div>
      }>
      {...λžœλ”λ§}
    </Suspense>
  );
}

async function getSchedulesMap(): Promise<Record<string, Schedule[]>> {
  const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/schedules`);
  const schedules: Schedule[] = await res.json();
  const record: Record<string, Schedule[]> = {};
  schedules?.slice(1)?.forEach((schedule) => {
    const date = format(new Date(schedule.departureTime), 'yyyy-MM-dd');
    if (record[date]) {
      record[date].push(schedule);
    } else {
      record[date] = [schedule];
    }
  });

  return record;
}

async function getRecentSchedule(): Promise<Schedule> {
  const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/schedules`);
  const schedules: Schedule[] = await res.json();
  return schedules[0];
}
  • Next.js 13 λ²„μ „μ—μ„œ μ œκ³΅ν•˜λŠ” μ„œλ²„ μ»΄ν¬λ„ŒνŠΈμ™€ React 18의 μ„œμŠ€νŽœμŠ€ μ‘°ν•©μœΌλ‘œ 데이터 μš”μ²­ μž‘μ—…μ„ μ§„ν–‰ν•΄λ³΄μ•˜μŠ΅λ‹ˆλ‹€.
  • 잘 λ™μž‘ν•˜μ˜€μœΌλ‚˜ 이후 revalidate, μž¬μš”μ²­ 이슈둜 λ‹€μ‹œ ν΄λΌμ΄μ–ΈνŠΈ μ‚¬μ΄λ“œμ—μ„œ μš”μ²­ν•˜λŠ” κ²ƒμœΌλ‘œ λ³€κ²½ν•˜μ˜€μŠ΅λ‹ˆλ‹€. (fetch ν•¨μˆ˜μ— μ˜΅μ…˜μ„ μ „λ‹¬ν•˜μ—¬ 주기적으둜 revalidate을 μš”μ²­ν•  수 μžˆμœΌλ‚˜ λžœλ”λ§ μ‚¬μ΄μ—λŠ” 기본으둜 μΊμ‹œλœ 값을 μ‚¬μš©ν•˜μ—¬ 데이터가 staleν•œ 값을 μœ μ§€ν•˜κΈ°μ— μ ν•©ν•œ 방법을 찾지 λͺ»ν–ˆμŠ΅λ‹ˆλ‹€.)

κ΄€λ ¨ commit λ‚΄μ—­ 1 κ΄€λ ¨ commit λ‚΄μ—­ 2


  • μŠ€νƒ€μΌλ§μ—λŠ” tailwindcssλ₯Ό μ‚¬μš©ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

데이터 μš”μ²­

// GET μš”μ²­
const { data: schedules } = useSWR<Schedule[]>(
  `${process.env.NEXT_PUBLIC_API_URL}/api/v1/schedules`,
  fetcher
);

// POST μš”μ²­
const { trigger, isMutating } = useSWRMutation(
  `${process.env.NEXT_PUBLIC_API_URL}/api/v1/schedules`,
  sendRequest,
  {
    onSuccess: (data) => {
      router.push(`/form/common/?id=${data.commonScheduleId}`);
    },
  }
);

// 카카였 API μš”μ²­
const { data, isLoading } = useSWR(
  () =>
    latitude &&
    longitude &&
    `https://dapi.kakao.com/v2/local/geo/coord2regioncode.json?x=${longitude}&y=${latitude}`,
  fetcherKakao
);
  • μ„œλ²„ μƒνƒœ 관리 라이브러리둜 SWR을 μ‚¬μš©ν–ˆμŠ΅λ‹ˆλ‹€.

  • μ„ μ–Έμ μœΌλ‘œ data 관리가 κ°€λŠ₯ν•˜κ³  onSuccess APIλ₯Ό 톡해 데이터 패칭 이후 둜직의 ꡬ성이 μš©μ΄ν•©λ‹ˆλ‹€.

  • GET μš”μ²­μ‹œ useSWR, POST μš”μ²­μ‹œ useSWRMutation APIλ₯Ό μ‚¬μš©ν–ˆμŠ΅λ‹ˆλ‹€.

  • 카카였 API, λ°±μ—”λ“œλ‘œ GET, POSTμš”μ²­μ— 따라 λ‹€λ₯Έ fetcher ν•¨μˆ˜λ₯Ό 미리 μž‘μ„±ν•˜μ—¬ μ‚¬μš©ν–ˆμŠ΅λ‹ˆλ‹€.


λ‘œλ”© UI κ΅¬ν˜„

if (isLoading || isMutating)
  return (
    <div className='w-full flex justify-center items-center mt-16'>
      <Spinner />
    </div>
  );

return ...
  • SWR을 μ‚¬μš©ν•˜μ—¬ 데이터 μš”μ²­ 사이 λ‘œλ”© UIλ₯Ό 톡해 μ‚¬μš©μžμ—κ²Œ λ‘œλ”© μ€‘μž„μ„ μ•Œλ €μ€λ‹ˆλ‹€.

쑰건뢀 μš”μ²­ 및 λ””λ°”μš΄μŠ€

const [latitude, setLatitude] = useState<number>();
const [longitude, setLongitude] = useState<number>();

const originName = searchParams.get('originName');
const origin = searchParams.get('origin');

const [query, setQuery] = useState('');
const debouceQuery = useDebounce(query, 1000);

const { data, isLoading } = useSWR(
  () =>
    debouceQuery &&
    longitude &&
    latitude &&
    `https://dapi.kakao.com/v2/local/search/keyword.json?x=${longitude}&y=${latitude}&query=${debouceQuery}`,
  fetcherKakao
);
  • API μš”μ²­μ— μΈμžκ°€ λ™κΈ°μ μœΌλ‘œ ν•„μš”ν•œ 경우 μœ„μ™€ 같이 μž‘μ„±ν•˜μ—¬ 쿼리가 μœ νš¨ν•œ μ‹œμ μ— 데이터 μš”μ²­μ„ ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
  • 검색 쿼리 μš”μ²­μ˜ 경우 λ„ˆλ¬΄ μž¦μ€ μš”μ²­μ„ λ°©μ§€ν•˜κΈ° μœ„ν•΄ λ””λ°”μš΄μŠ€ λ‘œμ§μ„ λ”°λ‘œ μ μš©ν–ˆμŠ΅λ‹ˆλ‹€.