[내배캠] 최종 프로젝트 회고

임홍원·2024년 2월 23일

캠퍼스팟 (Camperspot) 최종프로젝트 회고

📜 프로젝트 내용

캠핑장 예약 및 검색, 캠핑장 운영, 커뮤니티 플랫폼 '캠퍼스팟'

📅 개발일정

  • 2024.01 - 2024.02

👥 팀원 구성

⚙️ 기술 스택

⚙️ 기타 라이브러리

⚙️ BaaS


캠퍼스팟 핵심 기능

1. 캠핑장 검색 및 예약을 할 수 있습니다.

2. 캠핑장 운영 및 관리, 예약관리를 할 수 있습니다.

3. 커뮤니티를 통해 캠핑러들과 소통할 수 있습니다.


프로젝트에서 나의 역할

NextAuth를 이용한 인증 / 인가

NextAuth Credentials 활용하여 업체 회원 회원가입 & 로그인 구현
NextAuth Providers 활용하여 네이버 & 카카오 소셜 로그인 구현
Session 으로 유저 로그인 상태 확인
bcrypt 활용하여 회원가입시 비밀번호 암호화 후 저장

Next/Image 활용한 최적화

Image 컴포넌트를 활용하여 이미지 최적화 진행
Suspense 컴포넌트 활용하여 로딩 페이지 구현

Next.js Route Handlers

Route Handlers 를 활용하여 서버리스 API 구현

// api/auth/signup/route.ts
export const POST = async (req: NextRequest) => {
  const userData = await req.json();
  const { data } = await supabase
    .from('company_user')
    .select('email')
    .eq('email', userData.email)
    .single();

  if (data) {
    return NextResponse.json({
      status: 409,
      message: '이미 가입되어있는 이메일 입니다!',
    });
  }

  const hashedPassword = await bcrypt.hashSync(userData.password, 5);

  const { error: saveError } = await supabase
    .from('company_user')
    .insert<Omit<CompanyUserSignUpType, 'confirmPassword'>>({
      ...userData,
      password: hashedPassword,
    });

  if (saveError) {
    return NextResponse.json({
      status: saveError.code,
      message: '회원가입에 실패하였습니다!',
    });
  }

  return NextResponse.json({ status: 200, message: '회원가입 완료!' });
};

별점 리뷰 기능

Tanstack Query + CSS 로 별점 리뷰 기능 구현

Next.js middleware 라우트 가드 적용

업체 회원과 일반 유저가 url로 접근할 수 있으므로 특정 url에 접근하지 못하도록 라우트 가드 적용
이미 로그인 되어있는 회원 -> 로그인 url 접근 막기
로그인 안되어있는 회원 -> 로그인 url로 리다이렉트

커스텀 모달

React Portal 활용하여 커스텀 모달 구현

유저 프로필 변경

Tanstack Query 활용하여 유저 프로필 변경 기능 구현
유저 프로필 변경시 사용자 경험을 위해 이미지 업로드시 미리보기 구현

캘린더 CSS

UI / UX 측면에서 기존의 캘린더 디자인이 좋지 못하여 react-datepicker 디자인 커스텀 적용

실시간 유효성 검사

react-hook-form 을 활용하여 UX 높이기 위해 실시간 유효성 검사 진행

<div className={styles['content-container']}>
        <label>아이디</label>
        <input
          type='email'
          placeholder='예) email@gmail.com'
          required={true}
          {...register('email', {
            required: true,
            pattern: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
          })}
/>

비즈니스 로직과 UI 로직 분리

기존의 비즈니스 로직과 UI 로직이 한 군데에 존재하던것을 Custom Hook을 이용하여 분리

// useProfileQuery.ts
import { modifyUserData } from '@/app/profile/[id]/_lib/profile';
import { getUserData } from '@/app/profile/[id]/_lib/profile';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useParams } from 'next/navigation';

export const useProfileQuery = () => {
  const queryClient = useQueryClient();
  const params = useParams();
  const userId = params.id;

  const { data: profile, isLoading: isProfileLoading } = useQuery({
    queryKey: ['mypage', 'profile', userId],
    queryFn: getUserData,
    refetchOnMount: true,
  });
  const {mutate: profileMutate, isPending: isProfilePending} = useMutation({
    mutationFn: modifyUserData,
    onSuccess: () => {
      queryClient.invalidateQueries({
        queryKey: ['mypage', 'profile', userId],
      });
    },
  });

  return {profile, profileMutate ,isProfileLoading, isProfilePending}
}
'use client';
import Loading from '@/app/loading';
import Modal from '@/components/Modal';
import ModalPortal from '@/components/ModalPortal';
import { useProfileQuery } from '@/hooks/useProfileQuery';
...

const Profile = () => {
  const { show, toggleModal } = useModalStore();
  const { profile, isProfileLoading } = useProfileQuery();

  if (isProfileLoading) {
    return <Loading />;
  }

  return (
    <div className={styles.container}>
      <div className={styles['profile-container']}>
        <div className={styles['profile-header']}>프로필 관리</div>
        <div className={styles['profile-inner']}>
          <div className={styles['profile-image-wrapper']}>
            {profile?.profile_url && (
              <Image
                src={`${profile?.profile_url}` as string}
                width={120}
                height={120}
                alt='profile'
                priority
                className={styles['profile-image']}
                onClick={toggleModal}
              />
            )}
          </div>
          <div className={styles['nickname-container']}>
            <div className={styles['nickname-header']}>닉네임</div>
...
export default Profile;

트러블 슈팅

Next.js 캐싱문제

Next.js 이미지 캐싱 문제

Failed to execute 'json' on 'Response': body stream already read

Failed to execute 'json' on 'Response': body stream already read

최종 프로젝트 후

최종 프로젝트를 통해서 Next.js의 편의성에 대해서 크게 느꼈다.
특히 기존 Page Router가 아닌 14버전의 App Router를 사용했는데, getStaticProps 같은 함수들을 layout으로 활용해버리니 DX가 정말 많이 증가했다.
또한 Route Handlers를 활용하여 직접 API를 만드는것이 신기한 경험이었다. Next.js는 정말 기능이 많다.

최종 프로젝트이지만 너무 많은 기능을 넣어서 시간이 부족했다.
업체회원 기능을 빼면 아마 시간이 널널하게 리팩토링과 반응형 까지 완성 했을 것 같은 생각이 든다.
앞으로 못한 반응형과 지속적으로 유지보수를 하면서, 리팩토링, 기능 추가를 해야겠다.

5주동안 부족한 리더였지만 잘 따라와준 팀원들에게 감사를 보낸다.

profile
Frontend Developer

0개의 댓글