Skip to content

NextJS, Tailwind, Prisma, PlanetScale, Cloudflare를 사용해 serverless '당근마켓'을 클론코딩합니다. Serverless 'CARROT MARKET' clone coding using NextJS, Tailwind, Prisma, PlanetScale, Cloudflare.

Notifications You must be signed in to change notification settings

dition0221/next-carrot_market

Repository files navigation

Next - Carrot Market clone coding

NextJS, Tailwind, Prisma, PlanetScale, Cloudflare를 사용해 serverless '당근마켓'을 클론코딩합니다.

Serverless 'CARROT MARKET' clone coding using NextJS, Tailwind, Prisma, PlanetScale, Cloudflare.
  • "react": "^18"
  • "react-dom": "^18"
  • "next": "14.0.4"

Front-End :
Back-End :
3rd party :
etc :

thumbnail


미리보기

⭐ 결과물 : https://dition0221-next-carrot-market.vercel.app/


image

NextJS, Tailwind, Prisma, PlanetScale, Cloudflare 등을 사용한 serverless '당근마켓 클론 사이트'입니다.

  • API Route를 사용해 Back-End를 구현하였으며, SSR/SSG 등을 사용하여 SEO에도 신경썼습니다.
  • 무한스크롤 pagination을 사용하여, UX 상향DB의 부담 하락 장점을 적용하였습니다.
  • 해당 웹사이트는 middleware를 사용해 비로그인 사용자는 강제로 로그인 페이지로 이동하며, 접속국가가 외국이거나 을 사용한 경우 차단되도록 설정하였습니다.
  • PlanetScale DB가 4/8부로 유료로 전환이되기 때문에, 다른 DB로 전환할 예정입니다.

[회원가입 & 로그인 페이지]

  • E-Mail 또는 휴대폰번호를 통해 회원가입 및 로그인을 할 수 있습니다.
    • 네이버, 한메일, 다음, 카카오 이메일만 로그인 가능합니다.
    • 아쉽게도 휴대폰은 'Twilio' 유료 옵션 때문에 제대로 구현을 하지 못하였습니다.
  • E-Mail로 로그인 시 토큰 메일이 발송되며, 해당 토큰으로 인증 시 로그인이 완료됩니다.
    • 토큰 유효기간 3분
  • 소셜 로그인 개발 중 (NextAuth 사용 예정)

image

[홈 페이지]

  • 판매중인 물품들을 확인할 수 있는 페이지입니다.
  • 물픔은 상품 이미지, 제목, 가격, 총 찜 갯수를 표시합니다.
  • 우측에는 물품을 등록할 수 있는 floating 버튼이 위치해 있습니다.

image

[상품 페이지]

  • 해당 상품에 대해 판매자와 채팅방을 열 수 있으며, 찜 버튼을 통해 나의 찜 목록에 저장할 수 있습니다.
  • 이름이 비슷한 경우, 아래의 관련 상품들을 볼 수 있습니다.
  • 자신의 물품이라면, 물품 수정/삭제 버튼을 제공합니다.

image

[상품 등록 페이지]

  • 상품 이미지, 이름, 가격, 설명을 적을 수 있으며, 이미지를 제외한 부분은 필수요소입니다.
  • 이미지 파일 등록 시 이미지 미리보기를 제공합니다.
  • 이미지 등록 시 CloudFlare Images에 이미지 파일을 저장합니다.

image

[동네생활 페이지]

  • 사용자가 등록한 post들을 볼 수 있습니다.
  • 각 post마다 내용, 작성자, 작성시간과 더불어 '궁금해요'와 답변의 갯수를 포함합니다.
  • 우측 floating 버튼을 통해 게시물을 등록할 수 있습니다.

image

[동네생활 포스트 페이지]

  • 포스트의 답변 확인 및 답변 등록이 가능합니다.
  • '궁금해요'를 클릭해 on/off가 가능합니다. (count가 즉시 반영됩니다.)

image

[채팅 페이지]

  • 상품 페이지에서 판매자와의 채팅 버튼을 통해 생성된 채팅방 목록들을 보여줍니다.
  • 채팅 상대방의 닉네임, 아바타, 최신 채팅내역 및 시간을 보여줍니다.

image

[채팅방 페이지]

  • 상대방과 채팅을 주고받을 수 있습니다.
    • 아쉽게도 socketIo 등을 사용하지않아 실시간 채팅이 아닙니다.
  • 상단 화살표 버튼을 클릭해 이전 채팅 내용을 불러올 수 있습니다.
  • 현재 대화중인 상품을 확인할 수 있으며, 구매자는 '구매확정'을 할 수 있습니다.
  • 구매확정 시 리뷰를 등록할 수 있으며, 상품 및 채팅방이 삭제됩니다.
  • 나가기 시 채팅방이 삭제됩니다.

image

[라이브 페이지]

  • 라이브 스트리밍 목록들을 불러옵니다.
  • 우측 floating 버튼을 통해, 라이브를 시작할 수 있습니다.
  • ❗ 실제 라이브 스트림밍 기능은 동작하지 않습니다.

image

[라이브 상세 페이지]

  • 라이브 화면과 정보들을 확인할 수 있습니다.
  • 라이브 방 내에서 채팅을 주고받을 수 있습니다.

image

[나의 프로필 페이지]

  • 자신의 간단한 프로필과 판매/구매/관심 목록, 받은 리뷰 등을 보여줍니다.
  • Edit Profile : 아바타 이미지, 이름, 이메일주소, 휴대폰번호를 수정할 수 있습니다.
  • 판매/구매 내역 : 거래 끝난 내역들을 확인할 수 있습니다.
    • 거래 완료 시 CloudFlare images에서 이미지를 삭제하므로, 이미지는 확인 불가합니다.
  • 관심목록 : 찜한 상품들의 목록을 확인할 수 있습니다.

  • 24-01-02 : #3.0 ~ #4.8 / Set up + Tailwind CSS (1)
    • Set up (NextJS + Tailwind CSS)
      • 기본형 : npx create-next-app@latest
        • TypeScript, Tailwind CSS 사용
        • (강의를 따라갈 시) App Router 사용 x
      • Tailwind CSS를 따로 설치 시
        1. npm i -D tailwindcss postcss autoprefixer로 패키지 설치하기
        2. npx tailwindcss init -p로 설정파일 만들기
          • 'postcss.config.js'와 'tailwind.config.ts' 설정파일이 생성됨
        3. 'tailwind.config.ts' 파일에서 설정하기
          • 어느 파일에서 tailwind를 사용할 지 알려주어야 함
          • 'content' 내에서 작성
          • 기본형 : 경로/**(모든디렉토리)/*(모든파일).{확장자들}
            • ex.
              content: [
                './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
                './src/components/**/*.{js,ts,jsx,tsx,mdx}',
                './src/app/**/*.{js,ts,jsx,tsx,mdx}',
              ],
              
        4. 글로벌CSS 파일에서 아래의 코드를 추가하기
          • 기본형
            @tailwind base;
            @tailwind components;
            @tailwind utilities;
            
          • 'Unknown at rule @tailwind' 경고문 무시하기
    • Tailwind CSS
      • 유틸리티 우선 CSS framework
        • 유틸리티 : 아주 많은 class명을 가지고 있다는 뜻
        • 아주 많은 class명을 가지고 있는 CSS 파일
      • 사용법 : 요소에 적절한 class명을 추가하고 조합하여 사용
        • 반응형 디자인을 만들기 쉬움
      • 공식문서
      • Tailwind를 styled-components와 함께 사용 가능
    • World-class IDE Integration
      • Tailwind의 class명을 자동완성 기능을 제공해주는 확장프로그램
        • Tailwind는 많은 class명을 가지고 있어, 기억하기에 어렵기 때문
      • 설치법 : VSCode 확장프로그램에서 'Tailwind CSS IntelliSense'를 설치
      • 기능
        1. 자동 완성 기능 / 에러 표시 기능
        2. 색상 미리보기
        3. class명에 커서를 올릴 시 CSS를 보여줌
      • 자동 완성 기능이 바로 보이지 않을 시 'Ctrl + Space' 키를 누르기
    • Tailwind CSS의 문법 예시 (일부)
      • 배경색
        • 기본형 : bg-색상-채도
          • 채도 : [옵션] 50~950 사이의 값
        • ex. bg-transparent, bg-blue-500
      • 패딩 p (마진 m)
        • 기본형 : p-값
          • 상하좌우 : pt / pb / pl / pr
          • 가로세로 : px / py
        • 값 : 'rem' 단위 ('px'단위 사용 x)
          • rem : document font size를 기준으로하는 상대적인 크기
          • 자동완성 기능을 통해 미리 몇 px인지 알 수 있음
          • 또는 분수를 사용해 '%' 단위를 사용 가능
      • Border-Radius
        • 기본형 : rounded-크기
          • 크기 [옵션] { sm, md, lg, xl, full 등 }
      • Flex
        • 기본형 : flex
        • 방향 : flex-방향값
          • ex. flex-col
        • 자식요소 사이에 margin을 만들 수 있음 (gap)
          • 기본형 : space-방향-값
            • 방향 : { x, y }
            • 값 : rem 단위
          • 모든 자식요소에 자동으로 margin을 줌
      • Grid
        • 기본형 : grid
        • Gap : gap-값
          • 값 : rem 단위
      • Shadow
        • 기본형 : shadow-크기
          • 크기 : [옵션] { sm, md, lg 등 }
      • 정사각형
        • 기본형 : aspect-square
        • 가로길이를 정해주어야 함
          • 자동으로 세로길이를 똑같이 맞추어 줌
      • Ring
        • 요소의 테두리를 그리는 속성 (border와는 다름)
          • 'box-shadow'로 만들기 때문에 요소의 크기에 영향을 주지 않음
        • 기본형 : ring-값 / ring-offset-값 / ring-색상-채도 / ring-opacity-값
          • 값 : rem 단위
          • ring-offset : 요소와 ring 사이의 거리
        • 주로 상태 조건(focus 등)과 함께 사용되며, ring에만 상태 조건을 사용 시 다른 CSS속성들도 같이 적용됨
          • 같은 CSS variable을 공유하고 있기 때문
          • ex. focus:ring-2 ring-offset-2 ring-yellow-500
    • Modifier
      • 조건에 따른 CSS를 작성하는 기능
        • hover, focus 등
        • mobile only, screen only, 화면 방향, 인쇄 시 인쇄 스타일 등 가능
      • 기본형 : 조건:CSS문
        • ex. hover:bg-teal-500
        • ex. file:hover:bg-purple-400
        • 'transition' 사용 가능
      • 조건문 (일부 예시)
        • 상태 조건 : hover, focus, active, selection(블록드래그) 등
        • 자식 요소 : first, last, only, even, odd 등
        • empty, disabled
        • 반응형 웹 디자인
          • sm (@media (min-width: 640px))
          • md (@media (min-width: 768px))
          • lg (@media (min-width: 1024px))
        • dark (@media (prefers-color-scheme: dark))
      • 공식 문서
    • Form Modifier
      • <form> : 'focus-within' 등
      • <input> : 'required', 'valid', 'invalid', 'placeholder', 'placeholder-shown', 'disabled' 등
      • 노말 CSS에 있는 상태 조건들임
    • Group Modifier
      • 상위 요소에 의한 하위 요소의 스타일링
        • ex. 컨테이너에 커서를 올릴 시 특정 하위 요소의 스타일을 바꾸는 방법
      • 사용법
        1. 타겟하려는 그룹에 'group/이름' class명을 선언하기
          • ex. <div className="group/test" />
        2. 특정 하위요소에서 group에 대한 modifier를 사용하기
          • 기본형 : group-조건/이름:CSS문
            • ex. <div className="group-hover/test:bg-red-300" />
          • 'transition' 사용 가능
    • Peer Modifier
      • 형제 요소 선택자(~)에 대한 스타일링
        • ex. 사용자가 form을 제출 시 invalid 인지 아닌지 말해줄 수 있음
        • ex. input의 상태에 따라서 다른 요소의 스타일을 변화시킬 수 있음
      • 사용법 (Group Modifier와 사용법이 같음)
        1. 상태의 주체가 되는 요소에서 'peer/이름' class명을 선언하기
          • peer 요소는 선택자 보다 앞쪽에 위치해야 함
        2. 스타일을 변화할 요소에서 peer에 대한 modifier를 사용하기
          • 기본형 : peer-조건/이름:CSS문
    • <details>
      • 사용자에게 추가적인 정보를 제공하거나 숨겨진 콘텐츠를 표시하는 일반 HTML 태그
      • <summary> : <details>의 제목을 쓰는 부분
      • 사용자가 토글 버튼을 클릭 시 세부 정보 섹션이 열리거나 닫힘
        • 상태조건 open : 세부 정보 섹션이 열렸을 때의 상태
  • 24-01-04 : #4.9 ~ #5.2 / Tailwind CSS (2)
    • 반응형 웹 디자인
      • Tailwind는 mobile을 디자인한 후, desktop을 디자인하는 방식 (모바일 우선)
        • Tailwind에서느 모바일 화면을 위한 선택자가 없음
        • 모든 class명이 기본값으로 모바일에 우선 적용되고, 보다 큰 화면들을 위한 선택자가 존재함
      • 반응형 선택자
        • 시작점이 정해져 있지만, 끝점이 정해져 있지 않음 (무한대까지 적용)
        • sm (min-width: 640px)
        • md (min-width: 768px)
        • lg (min-width: 1024px)
        • xl (min-width: 1280px)
        • 2xl (min-width: 1536px)
        • portrait / landscape
    • 다크모드
      • 기본값으로, 사용자의 기기 설정에 따라 활성화되게 되어있음
      • 기본형 : dark:CSS문
      • 수동으로 다크/라이트 모드를 설정 가능
        • 'tailwind.config.js'에서 기기의 설정에 따를건지, 아니면 직접 토글시킬건지 설정하기
          • 자동 : darkMode: "media" (기본값)
          • 수동 : darkMode: "class"
            • 사용할 요소에 'dark'라는 class명을 넣어야지 작동함
              • 부모 요소 중에서 .dark를 찾는 기능을 함
            • <html> 또는 <body>에 class를 추가하는 것이 일반적
              • 모든 요소의 부모 요소이기 때문
              • '_app.tsx'에서 <Components>의 상위요소를 만들어도 됨
    • Just-In-Time Compiler (JIT Compiler)
      • Tailwind 3.0 이전 버전에서는 여러 개의 class를 중첩하여 사용하지 못했음
        • 수 많은 조합법이 있어, 아주 큰 CSS파일이 되기 때문
        • 배포 시 프로젝트를 스캔하여, CSS 파일에 포함된 class명을 제외하고 사용하지 않는 나머지 class들을 전부 삭제함 : purging
      • JIT 컴파일러는 코드를 실시간으로 감시하여, 필요한 class를 생성하는 기능
        • 작은 파일로 시작해, 사용한 class의 코드만 추가함
          • 개발자도구(F12)에서 <head> 내의 <style>을 통해 확인 가능
          • 선택자 중첩이 가능해짐
        • reset CSS가 존재함 (@tailwind base; 코드에 의함)
    • 사용자 정의 Tailwind CSS
      • 기본형 : '값' 부분에 [값] 형태로 사용
        • ex. text-[36px]
        • ex. text-[#fff]
        • ex. bg-[url('/vercel.svg')]
      • JIT Compiler 덕분에 사용이 가능해짐
    • 코드에 따라 Tailwind class를 바꿀 시 일종의 함수를 만듦
      • 템플릿 리터럴 대신 함수를 사용함
      • ex.
        function cls(...classNames: string[]) {
          return classNames.join(" ");
        }
        ......
        className={cls(
          "pb-4 font-medium",
          method === "email"
            ? "border-b-2 border-orange-500 text-orange-400"
            : ""
        )}
        
    • Tailwind plugins
      • Tailwind를 확장하는 재사용 가능한 써드파티 plugins
        • plugin : 부가적인 기능을 더해줌
        • 여러 개의 plugin들이 존재함
      • @tailwindcss/forms
        • <form>의 기본 스타일을 갖도록 해주는 plugin
          • <input>에 reset layer를 추가할 수 있음
        • 설치법 : npm i @tailwindcss/forms
        • 설정법 : 'tailwind.config.ts' 에서 'plugin' 프로퍼티의 배열에 'require("@tailwindcss/forms")'를 입력
          • 설정 후 자동으로 기본 스타일이 적용됨
      • 공식 문서
  • 24-01-05 : #5.3 ~ #5.11 / Tailwind CSS (3)
    • divide
      • 여러 개의 자식 컴포넌트 사이에 border를 그려주는 Tailwind만의 class명
        • 'space'와 비슷한 역할
      • 기본형 : divide-축-값
        • 축 : { x, y }
  • 24-01-06 : #5.12 ~ #5.15 / Tailwind CSS (4)
  • 24-01-06 : #5.16 ~ #5.18 / Tailwind CSS (5)
  • 24-01-10 : #6.0 ~ #6.4 / Prisma + PlanetScale (1)
    • Update : 컴포넌트 리팩토링
    • Prisma
      • JS(또는 TS)와 DB 사이에 다리를 놓아주는 번역기
        • Node.js and TypeScript ORM(Object Relational Mapping)
        • SQL 같은 DB언어를 작성하지 않아도 됨
          • { postgreSQL, MySQL, SQL Server, SQLite, MongoDB } 등에 사용 가능
      • 설치 및 설정법
        1. VSCode에서 'Prisma' 확장프로그램 설치하기
          • Syntax highlight, formatting, 자동완성 등의 기능
          • VSCode의 'settings.json' 파일에서 아래의 코드 추가하기 (Prettier를 위함)
            "[prisma]": {
              "editor.defaultFormatter": "Prisma.prisma",
              "editor.formatOnSave": true
            }
            
          • 참고 문서
        2. 'Prisma' 패키지 설치하기
          • npm i prisma -D
        3. 'Prisma' 초기화하기
          • npx prisma init
            • 'prisma' 폴더와 '.env' 파일이 생성됨 ('.env' 파일은 .gitignore에 등록할 것)
          • prisma CLI 사용 시의 접두사는 npx prisma
        4. '.env' 파일에서 'DATABASE_URL' 설정하기
        5. 'prisma/schema.prisma' 파일에서 datasource의 provider를 설정하기
          • provider : 사용할 DB의 종류
      • 공식 문서
    • Prisma에서 DB model 선언법
      • 'schema.prisma' 파일에서 데이터(모델)의 모양을 설명해주어야 함
      • Prisma가 자동으로 client를 생성해 줌 (자동완성 기능 제공)
        • client를 이용해 TS로 DB와 직접 상호작용할 수 있음
      • 기본형
        model 모델명 {
          컬럼명 타입 ...
          ......
        }
        
        • 옵션값 : 타입의 말미에 '?' 기호를 입력
        • @id : 해당 컬럼이 model의 id임을 알려줌
        • @unique : 중복값 금지
        • @default(필드값) : 기본값 설정
          • autoincrement() : 자동으로 증가하는 필드값
          • now() : 새 데이터가 만들어질 때 그 시점의 날짜를 가져옴
        • @updatedAt : 데이터가 업데이트 될 때의 시간을 자동으로 저장
      • ex.
        model User {
          id        Int      @id @default(autoincrement())
          phone     Int?     @unique
          email     String?  @unique
          name      String
          avatar    String?
          createdAt DateTime @default(now())
          updatedAt DateTime @updatedAt
        }
        
    • PlanetScale
      • MySQL과 호환되는 serverless DB 플랫폼
        • DB 플랫폼 : DB를 제공함
        • serverless : 직접 서버를 관리/유지보수할 필요가 없음 (서버가 없는 것이 아님)
        • Vitess
          • MySQL을 좀 더 쉽게 scaling 할 수 있도록 하는 오픈소스 시스템
          • Google이 YouTube를 scale하기 위해 만든 것
            • 대기업이 규모에 맞게 MySQL을 scale하기 위해 쓰는 방법
            • scale
              • 해당 데이터 값의 크기 또는 범위를 나타냄
              • 데이터의 정확성과 저장 공간을 조절하는 데 사용됨
      • 설치법
        1. 회원가입 및 로그인하기
        2. PlanetScale CLI 설치하기
          • 'scoop' 설치하기
            • 콘솔을 통해서 쉽게 다운로드 받을 수 있게 해주는 도구
            • powershell 관리자모드에서 irm get.scoop.sh -outfile 'install.ps1'iex "& {$(irm get.scoop.sh)} -RunAsAdmin" 명령어를 차례로 입력하여 설치
          • scoop으로 'pscale' 설치하기
            • [윈도우] 터미널에 아래와 같이 입력
              • scoop bucket add pscale https://github.com/planetscale/scoop-bucket.git
              • scoop install pscale mysql
        3. 터미널에 'pscale'를 입력해 제대로 설치되었는지 확인하기
        4. 터미널로 pscale 계정 로그인하기
          • pscale auth login
      • DB 생성법 (CLI 방법)
        • 홈페이지에서 DB를 생성하는 방법도 존재
        1. 지역 리스트 확인하기
          • pscale region list
        2. 가까운 지역에 DB를 생성하기
          • 기본형 : pscale database create 데이터베이스명 --region 슬러그명
            • ex. pscale database create carrot-market --region gcp-asia-northeast3
      • PlanetScale에서는 일종의 보안 tunnel을 이용 가능
        • 장점
          • 직접 DB의 PW를 관리하지 않아도 됨
          • MySQL을 설치/실행할 필요 없음
          • 2개의 DB를 만들어서 컴퓨터용/서버용으로 사용할 필요 없음
          • '.env' 파일에 PW를 저장할 필요 없음
        • PW없이 컴퓨터와 PlanetScale 사이에 보안 연결을 하는 방법
          1. DB와 연결하기
            • 기본형 : pscale connect 데이터베이스명
            • 연결을 유지해야 사용 가능
          2. '.env' 파일에 'DATABASE_URL' 입력하기
            • 기본형 : mysql://DB연결주소/DB명
  • 24-01-11 : #6.5 ~ #6.8 / Prisma + PlanetScale (2)
    • Vitess
      • MySQL과 호환되는 DB
        • MySQL과 비슷하지만, 다르게 처리함
      • PlanetScale은 Vitess를 사용함
      • 대량의 connections, tables과 다양한 서버들을 scaling 가능
      • MySQL에서는 하지만, Vitess에서는 하지않는 foreign key 제약
        • 일반 SQL은 DB에서 한 객체가 다른 객체에 연결된 상태를 생성하려고 할 때, DB가 자동으로 정보라는 것을 앎
          • DB에 저장 시 연결된 id주소를 확인함 (미존재 시 동작 x)
        • Vitess는 DB에 저장 시 연결된 id주소가 존재하는지 확인하지 않음
          • 에러없이 작동하기 때문에 Prisma를 이용해 도움을 받아야 함
          • DB 측에서 확인하는 것이 아니라, Prisma 측에서 확인함
        • Prisma의 도움을 받기 위한 설정법
          • 'schema.prisma' 파일에서 'datasource db' 객체에 relationMode = "prisma" 프로퍼티를 추가
    • Prisma schema를 기반으로 DB를 설정하고 동기화하는 방법
      • 명령어 : npx prisma db push
        • Prisma client가 생성됨
        • model schema가 반영됨
      • 홈페이지의 main branch에서 확인 가능
    • Prisma Studio 패키지
      • Visual Database Browser
        • DB를 위한 관리자 패널
      • 사용법 : npx prisma studio
    • Prisma client
      • 생각하는 방식으로 구성하고 앱에 맞춤화된 유형으로, Prisma schema에서 자동 생성되는 쿼리 빌더
        • TypeScript 및 Node.js용 직관적인 DB client
        • 'mongoose'와 같은 역할을 함
      • 'schema.prisma' 파일이 제공해줌
      • 자동으로 schema를 확인해 TypeScript로 타입을 만들어줌
        • '/node_modules/.prisma/client/index.d.ts'에서 확인 가능
      • 설치 및 사용법
        1. client 설치하기 npm i @prisma/client
        2. 초기화 코드 작성하기
          // '@/libs/client.ts'
          import { PrismaClient } from "@prisma/client";
          const client = new PrismaClient();
          export default client;
          
      • 사용법 : 'client' 객체를 이용해 사용
        • DB의 새로운 행(record)을 생성하는 방법
          await client.모델명.create({
            data: {
              내용
            }
          });
          
      • Front-End에서 Prisma client를 싱행하면 안 됨 (보안 문제)
        • Back-End에서 사용해야 함
    • NextJS의 API Routes
      • NextJS로 API를 빌드하기 위한 솔루션을 제공
        • server-side 전용 번들이며, client-side 번들 크기를 늘리지 않음
      • 파일 생성 : '/pages/api' 폴더 내에서 '.tsx' 파일을 생성
        • connection 핸들러인 함수를 export default 하여 사용함
      • 기본형
        import { NextApiRequest, NextApiResponse } from "next";
        export default function handler(
          req: NextApiRequest,
          res: NextApiResponse
        ) {
          ......
        }
        
      • ex.
        import { NextApiRequest, NextApiResponse } from "next";
        import client from "@/libs/client";
        export default async function handler(
          req: NextApiRequest,
          res: NextApiResponse
        ) {
          await client.user.create({
            data: {
              email: "hi",
              name: "hi",
            },
          });
          res.json({
            ok: true,
          });
        }
        
  • 24-01-12 : #7.0 ~ #8.5 / React-Hook-Form + Refactoring
    • React-Hook-Form 패키지
      • React에서 검증, 에러, 이벤트 같은 필요한 기능들을 넣어서 form을 만들 수 있게 해주는 패키지
      • 설치법 : npm i react-hook-form
    • React-Hook-Form 사용법
      • 선언법 : const { 요소들 } = useForm<제네릭>(옵션);
        • 옵션 'defaultValues' : field의 초기값 설정 가능
        • 옵션 'mode' : register의 검증 및 에러메시지가 나타나는 시점을 선택
          • [기본값] 'onSubmit'
      • register : [필수] <input>을 state와 연결시켜주는 메서드
        • 사용법 : <input {...register(이름, 옵션?)} />
        • 옵션을 사용해 각각의 field에서 검증 기능(+ 에러 메시지)을 사용 가능
          • 옵션 : { min, max, minLength, maxLength, pattern(정규식), validate 등 }
        • validate : 커스텀 검증 규칙을 생성하는 프로퍼티 (여러 개 가능)
          • 기본형 : validate : { 검증명 : (value) => 불리안값 || "메시지" }
          • ex.
            validate: { notGmail: (value) => !value.includes("@gmail.com") || "No Gmail"}
            
      • handleSubmit : [필수] form을 제출 시 사용하는 메서드
        • 사용법 : <form onSubmit={handleSubmit(유효시실행함수, 무효시실행함수?)}>
          • 주로 검증함수를 사용
        • 유효시실행함수 : const 변수명 = (data: 제네릭) => { ... };
        • 무효시실행함수 : const 변수명 = (errors: FieldErrors) => { ... };
      • watch : 콘솔창에서 확인할 수 있는 메서드
        • ex. console.log(watch());
      • reset : <form>의 모든 필드를 초기화하는 메서드
        • 사용법 : reset();
      • formState.errors : 에러 메시지
    • <form>에서 Back-End로 데이터를 보내는 방법
      • onSubmit을 통해 Back-End로 데이터를 전송
        • POST fetch를 사용
        • 기본형
          fetch(URL주소, {
            method: "POST",
            body: JSON.stringify(폼데이터),
            headers: {
              "Content-Type": "application/json",
            },
          });
          
        • headers의 '"Content-Type": "application/json"'이 있으면 object로 받음
          • 없으면 JSON문자열(원시 본문)로 받음
        • ex.
          fetch("/api/users/enter", {
            method: "POST",
            body: JSON.stringify(data),
            headers: {
              "Content-Type": "application/json",
            },
          });
          
      • API Route에서는 'req.body'로 <form> 데이터를 수신
        • ex.
          export default async function handler (
            req: NextApiRequest,
            res: NextApiResponse
          ) {
            if (req.method !== "POST") return res.status(405).end();
            console.log(req.body.email);
            return res.status(200).end();
          }
          
      • 반복되는 부분이 있기에 utility 함수(또는 Hook)를 만드는 것이 편함
    • 커스텀 React Hook
      • '.tsx' 파일을 생성해 컴포넌트의 함수, 변수 등을 return하여 사용
    • NextJS API Routes의 규칙
      • 무조건 함수를 export default 해야함
        • 함수를 return함으로써 NextJS에서 실행되기 떄문
        • 실행할 함수를 return하는 함수를 만드는 것
      • 고차함수 (HOF; Higher-Order Function)
        • 하나 이상의 함수를 인자로 받고, 함수를 return하는 함수
          • 함수를 다루는 함수
      • 커스텀 유틸리티 함수를 사용 시 handler 함수를 return 해야함
      • ex.
        // API Route
          async function handler(req: NextApiRequest, res: NextApiResponse) {
            console.log(req.body);
            return res.status(200).end();
          }
          export default withHandler("POST", handler);
        // Utility fn.
          export default function withHandler(
            method: "GET" | "POST" | "DELETE",
            fn: (req: NextApiRequest, res: NextApiResponse) => void
          ) {
            return async function (req: NextApiRequest, res: NextApiResponse) {
              if (req.method !== method) return res.status(405).end();
              try {
                await fn(req, res); // handler fn.
              } catch (error) {
                console.log(error);
                return res.status(500).json({ error });
              }
            };
          }
        
    • import문에서 절대경로 사용법
      • 'tsconfig.json' 파일에서 'compilerOptions' 객체에 아래와 같은 프로퍼티를 추가
        "paths": {
          "@/*": ["./src/*"]
        }
        
      • NextJS 프로젝트 생성 시 자동으로 설정 가능
        • React 프로젝트에서 수동으로 설정 가능
  • 24-01-16 : #9.0 ~ #9.2 / Authentication(1)
    • 휴대폰 번호를 사용한 회원가입/로그인 단계
      1. 폰 번호를 Back-End로 전송
      2. Back-End에서 DB에게 사용자의 폰 번호를 검색
        • DB에 사용자 정보가 존재하는 지의 여부를 알기 위함
          • (미 존재 시) 회원가입 시키기
          • (존재 시) 로그인 하기
      3. 사용자를 위한 토큰 생성
        • 토큰은 사용자와 연결되어 있음
          • User 모델과 관계를 짓는 Token 모델을 만들어야 함
      4. 난수를 사용하여, 사용자가 로그인 시 난수를 SMS로 발송
        • 검증을 통해 사용자는 토큰을 발급 받음
      5. Front-End에서 폰 번호 입력칸을 가리고, 난수(토큰) 입력칸을 추가하기
        • 토큰 입력 시 Back-End로 전송
      6. Back-End에서 토큰과 연결된 사용자 정보를 DB에서 찾아 가져오기
        • 사용자를 찾았다면 로그인 시키기
    • [Prisma] DB에서 데이터를 찾는 방법
      • 기본형
        const 변수명 = await 클라이언트.모델명.findUnique({
          where: {
            조건문,
          },
        });
        
        • 또는 .findFirst()를 사용
      • ex.
        const { phone, email } = req.body;
        const user = await client.user.findUnique({
          where: {
            email,
          },
        });
        
    • [Prisma] upsert 메서드
      • 데이터를 찾은 후, 있으면 업데이트 / 없으면 생성하는 메서드
      • 기본형
        const 변수명 = await 클라이언트.모델명.upsert({
          create: { 데이터가 없을 시 생성문 },
          update: { 데이터가 존재할 시 업데이트문 },
          where: { 조건문 },
        });
        
    • DB를 통해 사용자 정보를 체크한 후, 없다면 새로운 계정 생성
      • ex.
        const { phone, email } = req.body;
        const payload = phone ? { phone: +phone } : { email };
        const user = await client.user.upsert({
          where: { ...payload },
          create: {
            name: "Anonymous",
            ...payload,
          },
          update: {},
        });
        
      • 랜덤 닉네임 API
    • 토큰의 모델 스키마 생성
      • schema model에서 다른 schema model과 연결하는 방법
        • 기본형 : 컬럼명 모델명
        • 저장 시 자동 완성 기능으로 다른 정보들이 알아서 들어옴
        • 모델과 모델Id를 가지고 있는 이유 : DB에 실제 모델의 전체 데이터가 들어가지 않기 때문
        • 연결된 모델의 데이터 정보에 접근이 가능함
      • relationMode = "Prisma"에 대한 경고문이 나올 시 @@index([모델명 Id])를 추가하기
      • ex.
        model Token {
          id        Int      @id @default(autoincrement())
          payload   String   @unique
          user      User     @relation(fields: [userId], references: [id])
          userId    Int
          createdAt DateTime @default(now())
          updatedAt DateTime @updatedAt
          @@index([userId])
        }
        // User 모델에서는 'tokens Token[]' 컬럼이 추가됨
        
      • model schema 생성 후, PlanetScale에 넘겨주어야 함
        • 명령어 : npx prisma db push
    • 토큰 생성하기
      • 생성 시 model schema의 필수 요소들을 사용해야 함
      • user는 사용자 모델과 연결해야 함
        • { create?, connectOrCreate? , connect? }
          • connect : 새로운 Token을 이미 존재하는 User와 연결
          • create : 새로운 Token을 생성하면서, 새로운 User도 생성
          • connectOrCreate : User를 찾은 후, 있다면 Token과 연결 / 없다면 User를 생성
      • ex.
        // 'connectOrCreate'를 이용해 사용자를 'upsert'하는 코드와 합칠 수 있음
          // 조건을 만족하는 User가 있는 경우, Token과 연결
          // 없는 경우, User를 생성하고 Token과 연결
        const method = phone ? { phone: +phone } : email ? { email } : null;
        const payload = Math.floor(100000 + Math.random() * 900000) + "";
        const token = await client.token.create({
          data: {
            payload,
            method: {
              connectOrCreate: {
                where: {
                  ...method,
                },
                create: {
                  name: "Anonymous",
                  ...method,
                },
              },
            },
          },
        });
        
  • 24-01-18 : #9.3 / Authentication(2)
    • ISSUE : twilio에서 SMS 테스트 중 21608 에러 발생
      • 인증받은 번호라도 평가판으로는 제대로 작동하지 않는 것으로 보임
    • Twilio
      • 휴대폰 번호를 가지고 여러가지 통신 기능을 서비스해주는 플랫폼
        • SMS 발송, 전화 연결, 안심번호 등
        • 회원가입 시 무료 15$를 받을 수 있음
      • [콘솔] SMS 발송을 위한 설정법
        1. 'Messaging'-'Services'에 접속 후, 메시징 서비스 생성하기
        2. 'Sender Pool'에서 가상 전화번호 등록하기
          • 'Messaging'-'Try it out'-'Send an SMS'에서 전화번호를 받음
          • 가상 전화번호를 받을 수 있음 (가격 1$/월)
        3. SMS 테스트하기
          • 'Messaging'-'Try it out'-'Send an SMS'에서 테스트 가능
        4. '.env' 파일에 사용할 환경변수 저장하기
          • { 계정 SID, 인증 토큰, 서비스 SID 등 }
      • '네이버 클라우드 플랫폼'에서 SMS 기능을 사용할 수 있음
      • 홈페이지
  • 24-01-22 : #9.4 ~ #9.10 / Authentication(3)
    • [Twilio] 설치법 : npm i twilio
    • [Twilio] 코드로 SMS를 보내는 방법
      1. 'twilio client' 생성하기
        • 기본형
          import twilio from "twilio";
          const 변수명 = twilio(계정SID, 인증토큰);
          
      2. SMS 발송하기
        • 기본형
          await 트윌리오클라이언트.messages.create({
            messagingServiceSid: 메시지서비스SID,
            to: 받는사람휴대폰번호,
            body: 메시지내용,
          });
          
    • nodeMailer
      • NodeJS 환경에서 이메일을 쉽게 보낼 수 있게 도와주는 패키지
        • SMTP(Simple Mail Transfer Protocol) 서버를 통해 이메일을 보낼 수 있음
        • 네이버 메일로도 사용 가능 (SMTP 설정해야 함)
        • 구글은 하루에 500개 발송으로 제한
          • 'SendGrid' 같은 플랫폼을 이용 시 발송 제한이 없음
      • 설치법 : npm i nodemailer
        • 타입스크립트 : npm i -D @types/nodemailer
      • 설정법
        1. 앱 비밀번호를 환경변수로 저장하기 (구글 메일 사용 시)
          • '구글 계정 관리 - 보안 - 2단계 인증 - 앱 비밀번호'에서 16자리 비밀번호를 저장
        2. 'transporter' 변수 생성하기
          • 기본형
            import { createTransport } from "nodemailer";
            const 변수명 = createTransport({
              service: "플랫폼",
              auth: {
                user: 보내는사람메일주소,
                pass: 메일앱비밀번호,
              },
            });
            
          • 플랫폼 : { "gmail", "Naver" 등 }
      • 메일 발송 사용법
        1. 메일 옵션 설정하기
          • 기본형
            const 변수명: SendMailOptions = {
              from: 보내는사람메일주소,
              to: 받는사람메일주소,
              subject: 제목,
              text?: 내용(text),
              html?: 내용(html),
            };
            
        2. 메일 발송하기
          • 기본형 : await 트랜스포터.sendMail(메일옵션);
      • 참고자료
    • [Prisma] DB에서 삭제 시 설정법
      • 다른 model과 관계짓는 데이터를 삭제 시 에러가 뜰 수 있음
      • 해결법 : model schema에서 onDelete: Cascade 프로퍼티를 추가
        • 데이터 삭제 시 관계가 있는 model의 데이터도 같이 삭제되도록 함
          • Cascade : 부모 레코드가 삭제될 시 자식 레코드도 삭제하는 방법
      • prisma 수정 후 npx prisma db push 명령어를 통해 schema 변경 적용하기
      • ex.
        model Token {
          id        Int      @id @default(autoincrement())
          payload   String   @unique
          user      User     @relation(fields: [userId], references: [id], onDelete: Cascade)
          userId    Int
          createdAt DateTime @default(now())
          updatedAt DateTime @updatedAt
          @@index([userId])
        }
        
    • iron session
      • 서명, 암호화된 쿠키를 사용하는 NodeJS 무상태 session 도구 패키지
        • 사용자에게 쿠키를 주고, 사용자가 요청을 보냈을 때 누구인지 알 수 있도록 인증
        • JWT(Json Web Token)와는 다름
          • 웹에서 정보를 안전하기 전송하기 위한 토큰 기반의 인증 방식 중 하나
          • JWT는 정보를 JSON 형식으로 표현
          • 서버와 클라이언트 간의 인증을 위해 사용됨
          • JWT는 암호화되지 않고, 서명이 되었을 뿐임
            • 세션 DB를 사용하지 x
        • 세션 DB를 사용하지 않고도, 세션을 처리할 수 있는 패키지 (서버리스)
      • 작동 방식
        1. 사용자 정보를 암호화
        2. 이 암호화된 정보를 사용자에게 쿠키로 전송
        3. 사용자가 Back-End로 요청 시 쿠키를 같이 전송
        4. Back-End에서 쿠키를 복호화하여 사용자를 확인
      • 설치법 : npm i iron-session
      • 세션에 커스텀 프로퍼티를 추가해 업데이트 시 getIronSession()에서 Type(제네릭) 정의를 해주어야 함
      • 사용법 (생성 및 업데이트)
        • session 생성 기본형
          import { getIronSession } from "iron-session";
          const 세션명 = await getIronSession<인터페이스명>(req, res {
            password: 세션패스워드,
            cookieName: 쿠키이름,
          });
          
        • session 업데이트 기본형
          // 해당 session에 커스텀 프로퍼티를 추가 및 업데이트
          세션명.프로퍼티명 = 값;
          await 세션명.save();
          
      • 옵션
        • password : [필수] 쿠키를 암호화하는 데 사용하는 개인키
        • cookieName : [필수] 쿠키 저장 시의 쿠키명
        • ttl? : 세션 유효기간 (초 단위)
          • 기본값 14일
          • '0' 입력 시 iron-session이 자동으로 최대값을 적용함
        • cookieOption? : 'Set-Cookie' 속성이 아닌 경우를 제외한 쿠키 옵션 (문서 참고)
      • ex.
        interface IIronSessionData {
          user?: {
            id: number;
          };
        }
        const session = await getIronSession<IIronSessionData>(req, res, {
          password: process.env.SESSION_PW!,
          cookieName: "carrot-session",
        });
        session.user = {
          id: tokenExists.id,
        };
        await session.save();
        
      • 사용자 로그아웃 시 session을 만료시켜야 함
        • 기본형 : await 세션명.destroy();
      • 쿠키의 저장용량이 크지 않기 때문에, 많은 데이터를 저장하는 것은 좋지 않음
      • 홈페이지
      • 참고자료(세션, 토큰, 쿠키의 정의)
    • [Prisma] 데이터와 연결된 model schema 데이터를 가져오는 방법
      • DB 데이터의 기본값은 연결된 model의 id값만 가져옴
      • 기본형 : DB에서 데이터를 가져올 때 include: { 모델명: true }를 추가
        • prisma가 자동으로 model의 id값을 가지고 해당 테이블을 검색해 가져옴
      • ex.
        const tokenExists = await client.token.findUnique({
          where: { payload: String(token) },
          include: { user: true },
        });
        
    • 로그인 시 토큰 삭제하기
      • 토큰 인증 후 쓸모 없어진 토큰들을 DB에서 삭제해야 함
      • 기본형 : DB의 .deleteMany() 메서드를 사용해 해당 userId의 모든 토큰을 삭제
      • 로그인된 사용자가 로그인페이지에 오지 못하도록 막아야 함
    • NextAuth
      • NextJS에서 Authentication 구현을 도와주는 패키지
        • 패키지 설정(대부분 복붙)만 하면, 자동적으로 동작함
      • 특징
        • DB가 필요없음 (Back-End가 없음)
          • 사용자 기록을 저장하는 데이터가 없지만, DB에 저장이 가능하긴 함
          • 참고문서
        • 어떤 사용자가 로그인 됐는지는 알 수 없지만, 로그인 여부는 알 수 있음
      • 홈페이지
  • 24-01-24 : #10.0 ~ #10.4 / Authorization + SWR
    • Protected Route & API Route
      • 비로그인 사용자가 특정 페이지(또는 API 페이지)를 사용하지 못하도록 해야함
        • 비로그인 사용자가 API Route에서 session을 이용해 DB로부터 사용자를 찾는 행위는 에러가 발생하기 때문
        • { 로그인/비로그인 전용 페이지, 로그인/비로그인 전용 핸들러 }
      • API Route 보호하기
        • 'withHandler()' 커스텀 함수에서 로그인 유무에 대한 Boolean 값을 받는 인자를 추가함
        • '로그인전용 + 비로그인 사용자' 시 401 상태코드를 반환'
        • ex.
          interface IWithHandlerProps {
            handler: (req: NextApiRequest, res: NextApiResponse) => Promise<any>;
            isPrivate?: boolean;
          }
          export default function withHandler({
            handler,
            isPrivate = false,
          }: IWithHandlerProps) {
            return async function (req: NextApiRequest, res: NextApiResponse) {
              // Protected API route from user
              const session = await getSession(req, res);
              if(isPrivate && !session.user) {
                return res.status(401).json({ ok: false });
              }
              // Execute API route
              try {
                await handler(req, res);
              } catch {
                console.log(error);
                return res.status(500).json({ ok: false, error });
              }
            };
          }
          
      • Route 보호하기
        • Front-End에서 사용자 프로필을 확인할 수 있는 API Route로부터 fetch하여 가져옴
          • 커스텀 hook을 생성하여 간단하게 사용
          • 사용지 프로필이 존재 시 프로필 데이터를 반환
          • 미 존재 시 로그인페이지로 redirect
        • ex.
          export default function useUser() {
            const router = useRouter();
            const [user, setUser] = useState<Profile>();
            useEffect(()=>{
              (async () => {
                const data: IUserResponseType = await (await fetch("/api/users/me")).json();
                if (!data.ok) {
                  return router.replace("/enter");
                }
                setUser(data.profile);
              })();
            }, [router]);
            return user;
          }
          
        • redirect 시 화면 깜빡거림 현상이 있다면, middleware를 사용해 문제해결 가능
        • router.push()는 브랑줘 히스토리에 기록이 남지만, router.replace()는 기록하지 않음
        • 여러 개의 페이지에서 해당 hook을 사용 시 매번 API fetch를 해야하므로 좋지 않음
          • 캐싱을 이용해 모든 페이지에서 데이터를 공유하도록 해야함
          • 'SWR' 패키지를 사용
    • SWR 패키지
      • React hook 기반의 데이터 fetching 라이브러리
      • 특징 : SWR(Stale While Revalidate)은 HTTP 캐시 무효화 전략
        • 데이터를 rendering하기 전에 캐시된 데이터를 먼저 사용하고, 동시에 백그라운드에서 새로운 데이터를 가져오는 전략
        • 데이터가 업데이트 되었다면, 자동으로 업데이트된 데이터로 대체됨
          • 컴포넌트가 데이터의 변경을 계속 자동으로 감지할 수 있음
          • 다른 탭에 갔다가 돌아왔을 시 자동으로 데이터를 새로고침 해줌 (실시간처럼 느껴짐)
      • React-Query와 SWR 비교
      • 설치법 : npm i swr
      • 사용법 : const { data, error, isLoading, mutate 등 } = useSWR<제네릭>(URL주소, fetcher함수);
        • URL주소 : API를 요청할 URL이면서, 캐시를 저장할 때 사용할 key이기도 함
        • fetcher함수 : 첫 번째 인자(URL주소)로 요청을 보내는 함수
          • 데이터를 불러오고, 해당 데이터를 return하는 함수
        • mutate : 캐시 안에 저장된 data를 수정하는 함수
      • fetch 데이터를 캐시에 저장하므로, 앱의 어느 곳에서라도 같은 데이터를 사용 가능
        • 같은 key(URL주소)를 가진 fetch 데이터이어야 함
      • ex.
        const fetcher = async (url: string) => await (await fetch(url)).json();
        export default function useUser() {
          const router = useRouter();
          const { data, isLoading } = useSWR<IUserResponseType>("/api/users/me", fetcher);
          // If no-login, Redirect to "/enter"
          useEffect(()=>{
            if (data && !data.ok) router.replace("/enter");
          },[data, router]);
          return { user: data?.profile, isLoading };
        }
        
      • 홈페이지
    • [SWR] Global SWR Configuration (전역 설정)
      • 모든 useSWR() hook에 대한 기본값을 지정할 수 있는 전역 설정
        • useSWR()을 여러 번 사용 시 일일이 fetcher함수를 작성하거나 import하기가 꺼려지기 때문
      • 설정법 : 'App' 컴포넌트에서 <SWRConfig value={설정값}>으로 컴포넌트를 감싸줌
        • ex.
          // _app.tsx
          export default function App({ Component, pageProps }: AppProps) {
            return (
              <SWRConfig value={{fetcher: async (ulr: string) => await (await fetch(url)).json()}}>
                <Component {...pageProps} />
              </SWRConfig>
            );
          }
          // useUser.ts
          const { data, isLoading }= useSWR<IUserResponseType>("/api/users/me");
          
      • 공식문서
  • 24-01-25 : #11.0 ~ #11.4 / Product-page (1)
    • [Prisma] 10개 이상의 인스턴스가 있다는 warning
      • There are already 10 instances of Prisma Client actively running.
      • 발생 이유 : NextJS는 수정 시 마다 hot reloading 되기 때문 (서버를 완전히 껐다 키지 않음)
      • 해결법
        • 첫 실행 시에는 'PrismaClient' 객체를 생성하고, 개방 중에 이미 있다면 'global' 객체의 프로퍼티의 할당
        • TypeScript 사용 시 type 설정을 해주어야 함
        • ex.
          import { PrismaClient } from "@prisma/client";
          declare global {
            var prismaClient: PrismaClient | undefined;
          }
          const prismaClient = global.prismaClient || new PrismaClient();
          if (process.env.NODE_DEV === "development") global.prismaClient = prismaClient;
          export default prismaClient;
          
      • 공식문서
    • DB 개발 순서
      1. model schema 생성
      2. DB 업데이트: npx prisma db push
      3. mutation: form 데이터를 Back-End로 전송하여, DB에 저장
      4. DB로부터 데이터를 fetch
    • [Prisma] 'Products' 모델 schema 생성
      • String 타입은 길이 제한이 있는데, 더 늘리고 싶으면 @db.프로퍼티명으로 옵션을 부여
      • ex.
        model Product {
          id          Int      @id @default(autoincrement())
          createdAt   DateTime @default(now())
          updatedAt   DateTime @updatedAt
          user        User     @relation(fields: [userId], references: [id], onDelete: Cascade)
          userId      Int
          imageUrl    String
          name        String
          price       Int
          description String   @db.MediumText
          @@index([userId])
        }
        
      • model schema 생성/수정 후, 터미널에 npx prisma db push 입력하여 동기화하기
    • [Prisma] 'Product' model 데이터를 DB에 저장
      • ex.
        async function handler(
          req: NextApiRequest,
          res: NextApiResponse<IProductUploadResponse>
        ) {
          const { name, price, description }: IProductUploadForm = req.body;
          const { user } = await getSession(req, res);
          // Upload 'product' to DB
          try {
            const product = await prismaClient.product.create({
              data: {
                name,
                price,
                description,
                imageUrl: "TEST",
                user: {
                  connect: {
                    id: user?.id,
                  },
                },
              },
            });
            return res.status(200).json({ ok: true, product });
          } catch (error) {
            console.log(error);
            return res.status(500).json({ ok: false, error });
          }
        }
        
    • Back-End의 응답을 받아, Front-End에서 보여주기
      • 업로드페이지에서 form 데이터를 성공적으로 DB에 저장했다면, 해당 product 페이지로 이동시킬 것
      • ex.
        const router = useRouter();
        const [uploadProduct, { isLoading, data }] =
          useMutation<IProductUploadResponse>("/api/products");
        // <form> (react-hook-form)
        const { register, handleSubmit } = useForm<IProductUploadForm>();
        const onValid = (data: IProductUploadForm) => {
          if (isLoading) return alert("로딩 중 입니다.");
          uploadProduct(data);
        };
        // When finish uploading, Go to 'product detail' page
        useEffect(() => {
          if (data?.ok && data.product) {
            router.push(`/products/${data.product.id}`);
          }
        }, [data, router]);
        
    • DB로부터 데이터를 fetch하는 방법
      • REST API를 사용해 같은 주소로부터 GET, POST 방식으로 각각 다른 일을 하도록 만듦
      1. [Back-End] 'withHandler()' 커스텀 함수에서 method를 배열로 받게하기
        • ex.
          type Method = "GET" | "POST" | "DELETE";
          interface IWithHandlerProps {
            methods: Method[];
            handler: (req: NextApiRequest, res: NextApiResponse) => Promise<any>;
            isPrivate?: boolean;
          }
          // Check HTTP method in 'withHandler()' custom fn.
          if (req.method && !methods.includes(req.method as Method))
            return res.status(405).end();
          
      2. [Back-End] 핸들러 함수에서 req.method에 따라 각기 다른 일을 하도록 만들기
        • ex. if (req.method === "GET") { ... }
      3. [Back-End] 'GET' 방식일 때 DB로부터 특정 테이블 목록을 받아오기
        • 기본형 : const 변수명 = await PRISMA클라언트.모델명.findMany({});
      4. [Front-End] Back-End로부터 fetch하기
    • Product 상세 페이지 (/products/[id].tsx)
      1. [Front-End] dynamicURL의 쿼리값 받아오기
        • 기본형
          const 라우터변수 = useRouter();
          const { 쿼리변수명 } = 라우터변수.query;
          
      2. [Front-End] 해당 쿼리변수를 Back-End로부터 fetch하기
        • (라우터 마운트 시) 쿼리값이 'undefined -> 값'으로 변하기 때문에 주의 필요
          • 삼항연산자를 사용해 표현
          • ex. const { data } = useSWR(id ? `/api/products${id}` : null)
      3. [Back-End] 특정 Product 데이터를 DB로부터 받아오기
        • req.query를 통해 쿼리값을 받아옴
        • 데이터와 연결된 model의 전체 값을 가져오기 위해 include: { 모델명: true }를 사용
          • 특정 값만 가져오려면 include: { 모델명: { select: { 컬럼명: true, ... } } }
        • ex.
          const { id } = req.query;
          if (typeof id !== "string")
            return res
              .status(400)
              .json({ ok: false, error: "Only one dynamicParam is allowed" });
          const product = await prismaClient.product.findUnique({
            where: { id: +id },
            include: {
              user: {
                select: {
                  id: true,
                  name: true,
                  avatar: true,
                },
              },
            },
          });
          return res.status(200).json({ ok: true, product });
          
  • 24-01-26 : #11.5 ~ #11.6 / Product-page (2)
    • [Prisma] 연산자를 사용한 DB 검색 기능
      • DB에서 데이터를 검색 시 연산자를 이용해 특정 데이터를 가져올 수 있음
      • 비슷한 Product 이름을 가졌다면, similar items로 보여주려고 함
        • Product의 이름을 어절별로 나눈 후, 'OR' 연산자를 사용해 검색
        • ex.
          const terms = product.name
            .split(" ")
            .filter((word) => word !== "") // Except blank
            .map((word) => ({
              name: {
                contains: word,
              },
            }));
          const relatedProducts = await prismaClient.product.findMany({
            where: {
              OR: terms,
            },
          });
          
      • 공식문서
  • 24-01-27 : #11.7 / Product-page (3)
  • 24-01-29 : #11.8 ~ #11.10 / Product-page (4)
    • 관심상품(즐겨찾기) 기능 구현
      1. [Prisma] 관심상품 model schema 생성하기
        • ex.
          model Favorite {
            id        Int      @id @default(autoincrement())
            createdAt DateTime @default(now())
            updatedAt DateTime @updatedAt
            user      User     @relation(fields: [userId], references: [id], onDelete: Cascade)
            userId    Int
            product   Product  @relation(fields: [productId], references: [id], onDelete: Cascade)
            productId Int
            @@index([userId])
            @@index([productId])
          }
          
      2. [Back-End] API Route 생성하기
        • 'Product Id'로 DB에 검색 ➡ 존재 시 삭제 / 미 존재 시 생성
        • DB 사용 시 unique한 값을 사용하지 않기 때문에 .findFirst() 메서드를 사용
          • .findUnique() 메서드 사용 불가
      3. [Front-End] 관심상품 클릭 시 UI 업데이트하기
        • 새로고침 시 해당 제품이 관심상품인지 아닌지 알아야하므로, 관심상품 필드도 DB로부터 가져와야 함
        • ex.
          const isLiked = Boolean(
            await prismaClient.favorite.findFirst({
              where: {
                productId: +id,
                userId: user.?id,
              },
              select: {
                id: true,
              },
            })
          );
          
      4. [Front-End] 즉시 실시간으로 UI 업데이트하기
        • Optimistic UI Update : Back-End에 요청을 보낸 후, 응답을 기자리지 않고 즉시 변경사항을 반영하는 것
          • 낙관적(optimistic)이며, UI에서 요청이 정상적으로 잘 수행했다고 가정
          • 새로고침 없이 즉시 바뀌어야 하므로, SWR패키지의 mutate()를 사용
        • 기본형
          const { mutate } = useSWR(URL주소);
          mutate({ 데이터, 재검증여부 });
          
          • 데이터 : 원하는 어떤 데이터든 상관없이 사용 ➡ 새로운 데이터로 덮어씌움
            • 사용자 화면 UI의 변경사항을 보여주기 위한 부분
          • 재검증여부 : [Boolean] 변경이 일어난 후, 다시 API에서 데이터를 불러올지 결정
            • true 시 SWR은 즉시 첫 번째 인자를 데이터로 갱신하고, 모든 UI가 변경된 이후에 백그라운드에서 SWR이 URL주소로 최신 데이터를 fetch함
        • ex.
          // Fetch 'Product'
          const { data, mutate } = useSWR<IProductDetailResponse>(
            id ? `/api/products/${id}` : null
          );
          // Click 'Favorite'
          const [toggleFav, { isLoading: isToggleLoading }] = useMutation(
            `/api/products/${id}/favorite`
          );
          const onFavoriteClick = () => {
            if (isToggleLoading || !data) return;
            toggleFav({}); // DB
            mutate({ ...data, isLiked: !data.isLiked }, false);
          };
          // 또는
          mutate((prev) => prev && { ...prev, isLiked: !prev.isLiked }, false);
          
    • [SWR] useSWR()로부터 나온 데이터를 아무곳에서나 mutate()하는 방법
      • 기본형
        const { mutate } = useSWRConfig();
        mutate(키, 데이터, 재검증여부);
        
        • 키 : URL 주소
          • 인자로 키만 사용한다면, 새로 fetch하게 됨
        • 데이터 : 다른 컴포넌트의 데이터를 변경할 때는 화살표함수 형으로 작성
          • 자동으로 인자에 기존 데이터를 줌
      • 얽매인게 없기(unbounded) 때문에 변경시키려는 데이터를 정확하게 명시해야 함
      • ex.
        const { mutate } = useSWRConfig();
        mutate("/api/users/me", (prev: any) => ({ ok: !prev.ok }), false);
        
    • [Prisma] Product에 얼마나 많은 사람들이 Favorite을 눌렀는지 표시하는 방법
      • Product model에 Favorite model이 연결되어있어, 이것을 사용하면 됨
        • 새로운 컬럼을 생성해 직접 갯수를 지정하지 않아도 됨
        • ex.
          model Product {
            Favorites  Favorite[]
          }
          
      • 기본형
        const 변수명 = await PRISMA클라이언트.모델명.findMany({
          include: {
            _count: {
              select: {
                연결모델변수: true,
              },
            },
          },
        });
        
        • _count : relation을 카운트해주는 프로퍼티
      • ex.
        const products = await prismaClient.product.findMany({
          include: {
            _count: {
              select: {
                Favorites: true,
              },
            },
          },
        });
        
  • 24-01-31 : #12.0 ~ #12.3 / Community-page (1)
    • 동네생활 페이지 생성
      1. [Prisma] model schema 생성하기
        • 질문(Post), 답변(Answer), 궁금해요(Wondering) model 생성
      2. [Front-End] Post를 작성하는 <form> 생성하기
        • 폼 제출 시 폼데이터는 Back-End로 전송
      3. [Back-End] DB에 Post를 저장하기
        • ex.
          const post = await prismaClient.post.create({
            data: {
              question,
              user: {
                connect: {
                  id: user.id,
                },
              },
            },
          });
          
      4. [Front-End] DB에 Post 저장 완료 시 해당 Post로 이동하기
        • ex.
          const router = useRouter();
          // Submit form
          const [post, { data, isLoading }] = useMutation<IWriteResponse>("/api/posts");
          const onValid = (formData: IWriteForm) => {
            if (isLoading) return alert("로딩 중 입니다.");
            post(formData); // DB
          };
          // Succeed post => Go this post
          useEffect(()=>{
            if (data?.post) {
              router.push(`/community/${data.post.id}`);
            }
          }, [data?.post, router]);
          
      5. [Front-End] 해당 Post 페이지 보여주기
      • ex. const { data, error } = useSWR<ICommunityPostRes>(id ? `/api/posts/${id}` : null);
    • 동네질문 즐겨찾기 설정
      1. [Back-End] DB로부터 즐겨찾기 여부 확인하기
        • 확인 후, 존재 시 삭제 / 미 존재 시 생성
      2. [Front-End] 즐겨찾기 이벤트에 대한 UI 업데이트하기
        • ex.
          const [wonder] = useMutation(`/api/posts/${id}/wonder`);
          const onWonderClick = () => {
            if (!data || !data.post) return;
            mutate(
              {
                ...data,
                post: {
                  ...data.post,
                  _count: {
                    ...data.post._count,
                    Wonderings: data.isWondering
                      ? data.post._count.Wonderings - 1
                      : data.post._count.Wonderings + 1,
                  },
                },
                isWondering: !data.isWondering,
              },
              false
            );
            wonder({}); // DB
          };
          
  • 24-02-05 : #12.4 ~ #12.8 / Community-page (2)
    • 동네질문 Post에 대한 Answer 생성
      1. [Front-End] Answer 폼 데이터를 Back-End로 전송하기
      2. [Back-End] Answer에 대한 DB 처리하기
        • 먼저, Post가 존재하는지 확인 ➡ 미 존재 시 404 상태코드 반환
      3. [Front-End] UI 업데이트하기
        • DB에 정상적으로 저장되었으면, form을 reset
      4. [Front-End] 실시간UI 처리하기
        • useSWR()mutate를 사용해 optimistic으로 처리해도 되지만, 데이터 양이 많기에 re-fetch하는 것이 나음
          • 인수없이 mutate()만 사용하면 됨
        • ex.
          useEffect(() => {
            if (answerData?.ok) {
             reset(); // reset form
             mutate(); // refetch
            }
          }, [answerData?.ok, reset, mutate]);
          
    • 게시글 작성 시 위치정보 첨부
      • 게시글 작성 시 위치정보와 함께 작성되도록 할 예정
      1. 사용 편의성을 위해 커스텀 hook을 생성
        • ex.
          interface IUseCoordsState {
            latitude: number | null;
            longitude: number | null;
          }
          export default function useCoords() {
            const [coords, setCoords] = useState<IUseCoordsState>({
              latitude: null,
              longitude: null,
            });
            const onSuccess = ({
              coords: { latitude, longitude },
            }: GeolocationPosition) => {
              setCoords({ latitude, longitude });
            };
            // Get user's coordination
            useEffect(() => {
              navigator.geolocation.getCurrentPosition(onSuccess);
            }, []);
            return coords;
          }
          
      2. [Prisma] Post model schema 변경하기
        • latitude Float?, longitude Float?를 추가
      3. [Front-End] 현재 좌표를 보내어 근방의 Post들만 fetch하기
        • Post 리스트를 fetch 시 현재 사용자가 위치한 좌표를 전송해야 함
          • 쿼리파라미터('?')를 이용 (Back-End에서 'req.query'로부터 가져옴)
          • ex.
            const { latitude, longitude } = useCoords();
            const { data, isLoading } = useSWR<IPostList>(
              latitude && longitude
                ? `/api/posts?latitude=${latitude}&longitude=${longitude}`
                : null
            );
            
      4. [Back-End] 특정 좌표 범위 내에 있는 Post만 찾기
        • 적절한 위치 반경을 설정해야 함
          • 특정 좌표의 ±0.01 정도가 적당함
          • 또는 사용자가 직접 반경을 지정할 수 있도록 함
        • DB 검색 시 where 조건문에서 연산자 옵션을 사용
          • gte : 크거나 같음 (greater then or equal)
          • lte : 크거나 같음 (less then or equal)
        • ex.
          where: {
            latitude: {
              gte: latitudeNumber - 0.01,
              lte: latitudeNumber + 0.01,
            },
            longitude: {
              gte: longitudeNumber - 0.01,
              lte: longitudeNumber + 0.01,
            },
          },
          
  • 24-02-06 : #13.0 ~ #13.4 / Profile-page (1)
    • Update : [/api/users/me/records] API 리팩토링
      • model: Sale, Favorite, Purchase을 enum을 사용해 하나로 합쳐서 사용
    • Fix : [/api/enter.tsx] 로그인이 안 되는 현상 수정
    • Review 데이터
      1. [Prisma] Review model schema 생성하기
        • 한 model이 다른 한 model을 2번 이상 가리키는 경우, 문제 발생
          • 가리키는 model에서 name을 생성하고, fields값을 중복되지 않게 바꿔서 사용
          • 가리켜지는 model에서 @relation(name: 이름) 옵션값을 추가
        • ex.
          model Review {
            id           Int      @id @default(autoincrement())
            createdAt    DateTime @default(now())
            updatedAt    DateTime @updatedAt
            review       String   @db.MediumText
            createdBy    User     @relation(name: "WrittenReviews",  fields: [createdById], references: [id], onDelete: Cascade)
            createdById  Int
            createdFor   User     @relation(name: "ReceivedReviews",  fields: [createdForId], references: [id], onDelete: Cascade)
            createdForId Int
            score        Int
            @@index([createdById])
            @@index([createdForId])
          }
          model User {
            WrittenReviews  Review[]    @relation(name: "WrittenReviews")
            ReceivedReviews Review[]    @relation(name: "ReceivedReviews")
          }
          
        • 이름만 다르고, 구조가 같은 model이 여러 개가 있다면 enum을 사용 가능
          • ex.
            model Record {
              ......
              kind  Kind
            }
            enum Kind {
              Purchase
              Sale
              Favorite
            }
            
          • 사용 시 where조건문에서 'kind'만 설정하면 됨
            • 하나의 API만 사용해도 되는 장점이 존재함
            • API 요청 시 쿼리파라미터('?')를 사용
              • ex. /api/users/me/records?kind={kind}
      2. [Back-End] 받은 Review 목록에 대한 API 생성하기
        • ex.
          const { user } = await getSession(req, res);
          const reviews = await prismaClient.review.findMany({
            where: {
              createdForId: user.id,
            },
            include: {
              createdBy: {
                select: {
                  id: true,
                  name: true,
                  avatar: true,
                },
              },
            },
          });
          return res.status(200).json({ ok: true, reviews });
          
    • [Prisma] 이미 존재하는 model에게 새로운 컬럼을 생성할 때의 문제
      • DB에 이미 존재하는 데이터는 새 컬럼의 값이 없기 때문에 문제가 발생
      • 해결법 (다음 중 택1)
        1. 모든 DB를 초기화하는 방법
        2. 새 컬럼의 값을 옵셔널로 지정
        3. 새 컬럼의 값에 기본값을 지정
          • 기본형: @default(기본값)
          • ex. score Int @default(1)
  • 24-02-07 : #13.5 ~ #13.6 / Profile-page (2)
    • 사용자 프로필 업데이트
      • react-hook-form 패키지의 setValue를 사용해 session의 사용자 정보를 미리 <input>에 넣어둠
      • Back-End에서 DB의 데이터와 중복 체크 후 업데이트하기
        • 새로운 프로필 내용만 가져다가 사용
          • ex. const newName = name && name !== currentUser?.name ? name: undefined;
        • DB로부터 unique한 컬럼값이 중복인지 확인
          • 중복 시 error
          • 미 중복 시 업데이트
  • 24-02-13 : #14.0 ~ #14.6 / Streams
    • 라이브 스트림 페이지 구현
      1. [Prisma] 라이브와 라이브챗메시지 model schema 생성하기
        • ex.
          model Stream {
            id          Int       @id @default(autoincrement())
            createdAt   DateTime  @default(now())
            updatedAt   DateTime  @updatedAt
            name        String
            description String    @db.MediumText
            price       Int
            user        User      @relation(fields: [userId], references:  [id], onDelete: Cascade)
            userId      Int
            Messages    Message[]
            @@index([userId])
          }
          model Message {
            id        Int      @id @default(autoincrement())
            createdAt DateTime @default(now())
            updatedAt DateTime @updatedAt
            message   String   @db.MediumText
            user      User     @relation(fields: [userId], references:  [id], onDelete: Cascade)
            userId    Int
            stream    Stream   @relation(fields: [streamId], references:  [id], onDelete: Cascade)
            streamId  Int
            @@index([userId])
            @@index([streamId])
          }
          
      2. [Front-End] 라이브를 시작하는 form 생성하기
        • react-hook-form 패키지의 register에서 valueAsNumber: true 옵션을 부여하면, 값이 number 형태가 됨
      3. [Back-End] DB에 저장하는 API 생성하기
        • ex.
          const { name, price, description }: ICreateLiveForm = req.body;
          const stream = await prismaClient.stream.create({
            data: {
              name,
              price,
              description,
              user: {
                connect: {
                  id: user.id,
                },
              },
            },
          });
          
      4. [Front-End] DB로부터 데이터를 fetch하기
    • 라이브 채팅 시스템에서 실시간 같은 기능 구현
      1. [Back-End] Stream model을 가져올 때, include를 사용해 Message model도 가져오기
        • ex.
          const stream = await prismaClient.stream.findUnique({
            where: {
              id: +id,
            },
            include: {
              Messages: {
                select: {
                  id: true,
                  message: true,
                  user: {
                    select: {
                      id: true,
                      avatar: true,
                    },
                  },
                },
              },
            },
          });
          
      2. [Front-End] Message 리스트로부터 메시지 가져오기
        • ex.
          {data?.stream?.Messages.map((msg) => (
            <Message
              key={msg.id}
              text={msg.message}
              reversed={msg.user.id === user?.id ? true : false}
            />
          ))}
          
      3. [Front-End] 눈속임으로 사용자가 보낸 메시지를 실시간으로 구현하기
        • NextJS는 serverless이기 때문에 실시간을 만들 수 없음
          • 웹소켓서버를 이용해야지 실시간 구현 가능
        • mutate()를 사용해 re-fetch하는 방법
          • 인수를 사용하지 않으면, re-fetch를 함
        • 가짜 데이터 트릭을 사용하는 방법
          • mutate(데이터, 재확인여부)를 사용해 폼데이터를 추가함
          • ex.
            mutate(
              (prev) =>
                user &&
                prev?.stream && {
                  ...prev,
                  stream: {
                    ...prev.stream,
                    Messages: [
                      ...prev.stream.Messages,
                      {
                        id: Date.now(),
                        message: formData.message,
                        user: {
                          id: user.id,
                          avatar: user.avatar,
                        },
                      },
                    ],
                  },
                },
              false
            );
            
      4. [Front-End] useSWR()refreshInterval옵션을 사용해 자동으로 fetch하여 실시간으로 보이도록하기
        • 기본형 : useSWR(URL주소, { refreshInterval: 밀리초 });
    • [Prisma] seeding
      • 초기 데이터를 DB에 삽입하는 프로세스
        • 주로 개발 및 테스트 목적으로 사용
        • App을 처음으로 설정하거나 새로운 개발 환경을 구성할 때, DB에 테스트용 데이터를 채우는 데 사용
      • 사용법
        1. 파일 생성하기
          • 파일명 : prisma/파일명.ts
          • ex.
            import { PrismaClient } from "@prisma/client";
            const prismaClient = new PrismaClient();
            async function main() {
              [...Array.from(Array(500).keys())].forEach(async (item) => {
                const stream = await prismaClient.stream.create({
                  data: {
                    name: String(item),
                    description: String(item),
                    price: item,
                    user: {
                      connect: {
                        id: 11,
                      },
                    },
                  },
                });
                console.log(`${item}/500`);
              });
            }
            try {
              main();
            } catch (error) {
              console.log(error);
            } finally {
              () => prismaClient.$disconnect();
            }
            
        2. ts-node 패키지 설치하기
          • 설치법 : npm i ts-node
        3. 스트립트 생성하기
          • 'package.json' 파일에서 생성
            • ex.
              "prisma": {
                "seed": "ts-node prisma/파일명.ts"
              }
              
            • npx prisma db seed라는 명령어를 통해 실행 가능해짐
          • Cannot use import statement outside a module 에러 발생 시
            • "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/파일명.ts"로 수정
      • 공식문서
    • [Prisma] DB에 연결하는 갯수 설정
      • 기본형 : DB URL에 ?connection_limit=연결갯수를 입력하여 사용
      • 그 외에도 다른 옵션들 설정 가능
    • [Prisma] pagination
      • DB로부터 데이터를 가져올 때, 여러 페이지로 나누어 한 번의 일정량의 데이터만 표시하는 방법
      • 옵션 기본형
        • take : [number] 가져올 데이터 갯수
        • skip : [number] 건너뛸 데이터 갯수
      • 관계된 다른 model(include)에서도 사용 가능하며, 응답에 포함시킬 경우 pagination을 권장
        • DB 사용 시 최소한의 데이터만 가져오는 것이 좋음
          • pagination과 select를 사용하는 것을 권장
      • Front-End에서는 state변수와 쿼리파라미터('?')를 사용해 Back-End에 전송
  • 24-02-14 : Infinite scroll pagination (1)
    • ISSUE : ✅ [/pages/streams/index.ts] 무한스크롤을 사용한 pagination
      • 고려사항 : react-query 도입? SWR로 잘 되지 않음
        • useSWRInfinite()를 사용하자니 isLoading이 바뀌지 않아 runtime error
        • useSWR()를 사용하자니, 불분명 원인에 의해 같은 데이터를 2번씩 fetch되는 문제 => key 중복 문제, 게다가 다른 페이지에 갔다가 돌아오면 제대로 동작하지 않음. 이 방법은 아닌듯.
  • 24-02-15 : Infinite scroll pagination (2)
    • UPDATE : 무한스크롤을 사용한 pagination
      • 최대한 SWR 패키지를 이용해 구현하기
      • 'react-infinite-scroller' 패키지가 문제 있는 게 아닐까?
        • 2년 전부터 패키지 업데이트가 없음 ➡️ 다른 방법 모색
      • 'react-intersection-observer' 패키지 사용하기
    • ISSUE : ✅ 첫 동작, 스크롤 동작 시 2번씩 실행되는 문제
      • FIX : 동작 여부의 boolean 변수를 생성하고, 동작 후 setTimeout()을 사용해 일정시간동안 막아둠
      • 참고자료
    • ISSUE : ✅ 더 이상 불러올 데이터가 없음에도 불구하고, 계속 시도함
      • 원인 : useSWRInfinite()isLoadingisValidating 때문
      • FIX : setSize()의 조건을 inView와 스크롤가능여부(boolean)만 사용
        • isLoading과 isValidating 제거
    • 무한스크롤을 사용한 pagination
      • useSWRInfinite()을 사용한 데이터 fetch
        • pagination을 하기 편하게 도와주는 hook
          • 여러 개의 데이터를 배열에 담아서 줌
        • 사용법
          const { data, size, setSize, isValidating, isLoading 등 } = useSWRInfinite<데이터제네릭>(getKey함수, ?Fetcher함수, ?옵션);
          const getKey = (pageIndex: number, prevData: 데이터제네릭) => {
            if (prevData && !prevData.length) return null; // Reached the end
            return `URL주소?쿼리파라미터=${pageIndex}`;
          };
          
        • 공식문서
        • 참고자료
    • react-intersection-observer 패키지
      • React App에서 Intersection Observer API를 쉽게 사용할 수 있도록 도와주는 패키지
        • 요소가 뷰포트(화면)에 들어오거나 나갈 때 알려줌
          • 요소의 가시성과 관련된 이벤트를 처리하는 데 편리한 컴포넌트와 hook을 제공
        • Intersection Observer API
          • 브라우저에서 제공하는 기능으로, 요소들의 교차 영역(intersection)을 비동기적으로 감시할 수 있게 해주는 기능
          • 요소들이 화면에 보이는지 여부를 쉽게 감지하고, 그에 따른 작업을 수행 가능
      • 설치법 : npm i react-intersection-observer -D
      • 사용법 : const { ref, inView 등 } = useInView();
        • ref : 참조 요소, ref 프로퍼티에 할당
        • inView : [Boolean] 참조 요소가 화면에 보이는지의 여부
      • 공식문서
      • 참고자료
    • 무한스크롤 pagination ex.
      /* Custom hook */
        export default function useInfiniteScroll<T = any>(url: string) {
          const [isScrollLoading, setIsScrollLoading] = useState(true);
          const [isKillScroll, setIsKillScroll] = useState(false);
          // Prevent duplicate runs at the first time
          // ! 데이터가 없는 경우, 2번 동작함
          useEffect(() => {
            setTimeout(() => setIsScrollLoading(false), 1000);
          }, []);
          // Fetch data
          const getKey = (pageIndex: number, prevData: T) => {
            if (prevData && (prevData as any).ok === false) {
              setIsKillScroll(true);
              return null; // if 'ok: false', Reached the end
            }
            return `${url}?page=${pageIndex}`;
          };
          const { data, setSize, isLoading, isValidating } = useSWRInfinite<T>(getKey);
          // Scroll fn.
          const { ref, inView } = useInView(); // Watch viewport
          useEffect(() => {
            if (inView && !isScrollLoading && !isKillScroll) {
              setIsScrollLoading(true);
              setSize((prev) => prev + 1);
              setTimeout(() => setIsScrollLoading(false), 1000); // Prevent duplicate runs
            }
          }, [inView, isScrollLoading, setSize, isKillScroll]);
          return { data, ref, isLoading: isLoading || isValidating };
        }
      /* In use */
        const { data, ref, isLoading } =
          useInfiniteScroll<IStreamsResponse>("/api/streams");
        return (
          <section>
            {(data ?? []).map((page) =>
              page.streams?.map((stream) => (
                <Link
                  href={`/streams/${stream.id}`}
                  key={stream.id}
                >
                  <h1>{stream.name}</h1>
                </Link>
              ))
            )}
          </section>
          {isLoading ? <div>Loading..</div> : null}
          <div ref={ref} />
        );
      
  • 24-02-16 : #15.0 ~ #15.8 / Cloudflare Images
    • Cloudflare
      • 인터넷에 연결하는 모든 것을 안전하고 비밀을 유지하면서, 신속하고 안정적으로 연결하도록 설계된 전역 네트워크
    • Cloudflare Images API
      • 대규모로 이미지를 저장, 크기 조정, 최적화하는 하나의 API
        • 이미지 인프라를 구축하고 유지하는 효율적인 솔류션을 제공함
        • 하나의 통합 제품을 이용해 이미지를 대규모로 저장, 크기 조정, 최적화 함
      • 요금제
        • 저장 : 5 달러 / 10만 개
        • 전송 : 1 달러 / 10만 개
        • 대역폭 요금이 없어서 이미지 용량에 신경쓰지 않아도 됨
        • 크기 조정, 최적화에 추가 요금 x
    • react-hook-form에서 input:file 다루는 방법
      1. file을 제출하는 register 생성하기
        • 타입은 FileList를 사용
        • ex.
          {...register("avatar", {
            validate: {
              isImage: (value) =>
                (value && value[0].type.includes("image")) ||
                "이미지 파일만 업로드 가능합니다.",
            },
          })}
          
      2. file 변경을 감지하는 이벤트 리스너 생성하기
        • 이미지 파일이 변경된다면, 화면에 미리보기로 보여주기
          • useForm()watch()를 사용해 변경을 감지
          • watch(?INPUT명) : form의 변경을 감지할 수 있음
        • ex.
          const avatar = watch("avatar");
          useEffect(()=>{
            if (avatar && avatar.length > 0) {
              const file = avatar[0];
            }
          }, [avatar]);
          
      3. file의 URL을 알아내기
        • file을 선택하고나면 browser의 memory에 저장되므로, 해당 URL주소를 알아내야 함
        • 기본형 : URL.createObjectURL(파일변수);
          • 'blob' 글자를 포함한 URL주소를 사용해야 함
      4. 미리보기 이미지 제공하기
        • useState()를 이용해 <img>의 src로 제공
        • ex.
          const [avatarPreview, setAvatarPreview] = useState("");
          const avatar = watch("avatar");
          useEffect(()=>{
            if (avatar && avatar.length > 0) {
              const file = avatar[0];
              setAvatarPreview(URL.createObjectURL(file));
            }
          },[avatar]);
          return {avatarPreview ? <img src={avatarPreview} /> : null}
          
    • Cloudflare 이미지 업로드
      • [콘솔] image dashboard를 사용하는 방법
        • 관리자 권한이 있는 사람만 사용 가능
      • [코드] API token을 사용하는 방법
        • client ➡️ server ➡️ Cloudflare 순으로 업로드를 하기 때문에, 불필요하게 server에서 사용해 대역폭을 낭비하는 문제
      • [코드] Direct Create Upload 방법 ✅
        • 사용자의 browser가 Cloudflare에 직접 업로드하는 방법
          • 사용자의 이미지를 직접 다루지않아, 비용절감
        • 보안URL을 통해 업로드 (token을 노출하지 않음)
          • 실행 방식
            1. 사용자가 파일 업로드 요청
            2. [Back-End] API key와 함께 Cloudflare에 요청
            3. Cloudflare는 빈 파일 URL(1회용)을 Back-End에 걸쳐 사용자에게 전달
            4. 사용자는 해당 URL을 통해 Cloudflare에 직접 파일 업로드
          • 사용자가 파일을 업로드하지 않을 시 30분 뒤에 자동으로 URL이 삭제됨
        • 사용법 (먼저 번들 구매)
          1. 'images - 개요 - API 사용'에서 API token 얻기
            • 커스텀 token 생성 후, .env파일에 저장
              • 권한 : 계정 / Cloudflare Images / 편집
            • 계정 ID도 .env파일에 저장
          2. [Back-End] Cloudflare URL 요청하기
            • 기본형
              const 변수명 = (await (
                await fetch(
                  `https://api.cloudflare.com/client/v4/accounts/${process.env.CF_ID}/images/v2/direct_upload`,
                  {
                    method: "POST",
                    headers: {
                      Authorization: `Bearer ${process.env.CF_TOKEN}`,
                    },
                  }
                )
              ).json());
              
            • 사용자는 URL주소를 통해 이미지를 저장
          3. [Front-End] form을 사용해 Cloudflare URL로 파일 업로드하기
            • 기본형
              const 변수명 = new FormData();
              변수명.append(키명, 값, ?파일명);
              // 파일명 미 입력 시 원래 파일명으로 대체
              
            • 업로드의 결과로 주어지는 id를 통해 Cloudflare에 접근가능하므로, DB에 저장해야 함
            • ex.
              // Ask for Cloudflare URL
                const cloudflareUrl = await (await fetch("/api/files")).json();
              // Error handling
                if (!cloudflare.ok) return;
              // Submit form with avatar file to Cloudflare
                const form = new FormData();
                form.append("file", avatar[0]);
                try {
                  const uploadAvatar = (await (
                    await fetch(cloudflareUrl.url, {
                      method: "POST",
                      body: form,
                    })
                  ).json());
                } catch (error) {
                  return alert("Fail: ", error);
                }
              
      • 공식문서
    • Cloudflare 이미지 가져오는 방법
      • [콘솔] 이미지 제공 URL을 복사하여, src로 사용하기
        • 기본형 : https://imagedelivery.net/<계정해시>/<image_id>/<variant_name>
          • 계정해시 : 콘솔에서 확인가능
          • image_id : 사용자 DB를 통해 가져와 사용
          • variant_name : 리사이징값, 원본은 public으로 입력해 사용
      • ex.
        const = { user } = useUser(); // 세션으로부터 사용자 정보 가져오기
        {user?.avatar ? (
          <img
            src={`https://imagedelivery.net/<계정해시>${user?.avatar}/public`}
          />
        ) : null}
        
    • Cloudflare 이미지 리사이징(re-size, 크기 조정)
      • 작은 곳에 큰 이미지가 사용되면 낭비이기 때문에 리사이징을 사용함
        • Cloudflare가 무료로 자동적인 리사이징을 제공함
      • 설정법 : [콘솔] 'Images-변형'에서 새로운 <variant_name> 생성 및 편집
        • Scale down : 비율의 맞춰 이미지의 크기를 맞춤
        • Contain : 비율에 맞춰 이미지의 크기를 가능한 맞춤
        • Cover : 비율에 맞춰 딱 원하는 이미지의 크기를 맞추나, 튀어나온 부분은 자름
        • Crop : 비율 상관없이 딱 원하는 이미지의 크기를 맞춤
        • Pad : 비율에 맞춰 이미지의 크기를 맞추며, 남는부분에 흰색 padding 적용
      • 사용법 : 이미지를 가져올 때 <variant_name> 부분에서 변수를 사용
  • 24-02-20 : #16.0 ~ #16.1 / NextJS Images (1)
    • NextJS에서의 이미지
      • HTML <img> 대신 NextJS image component(next/image)를 사용해야 함
        • 무료로 이미지 최적화를 할 수 있기 때문
      • 장점
        1. lazy loading
          • 사용자는 이미지가 나올 때까지 스크롤하지 않으면, 이미지를 load하지 않음
            • 모든 이미지를 즉시 download 하지 않음
        2. placeholder
          • local 이미지 로딩 시 이미지가 흐리게 보이다가 load 됨
            • 이미지 로딩 시 블러처리 된 placeholder 이미지를 제공
    • 이미지 파일 종류
      • local image : 'public' 폴더 내에 존재하는 이미지 파일
        • API 응답 전에 framework가 인지하는 이미지
      • remote image : API 응답으로부터 가져오는 이미지 파일
        • local image만큼 최적화되지 않음 (파일 시스템에 없기 때문)
    • Local image의 <Image> 사용법
      • 기본형
        import Image from "next/image";
        import 이미지변수 from "public/이미지경로";
        <Image src={이미지변수} />
        
      • 옵셔널 프로퍼티
        • placeholder=blur : 로딩 시 블러치리 된 placeholder 이미지를 사용
        • quality : [백분율] 이미지 품질 (기본값 75)
        • width : [px] 이미지 가로크기
        • height : [px] 이미지 세로크기
      • NextJS가 자동으로 src URL을 만듦 (inspect에서 확인 가능)
        • 이미지를 압축하기 때문 (_next/image API handler로 부터)
        • src URL : '/_next/image?URL핸들러'
          • q : [백분율] 품질
      • 공식문서
  • 24-02-21 : #16.2 ~ #16.4 / NextJS Images (2) + [Challenge] Chat system (1)
    • FIX : [무한스크롤] 데이터가 없는 경우, 2번 동작함 해결
      • 조건없는 1초 후 setTimeout()을 조건부로 변경
      useEffect(() => {
        if (data && (data as any)[0].ok)
          setTimeout(() => setIsScrollLoading(false), 1000);
      }, [data]);
      
    • UPDATE : 모든 <img>태그를 <Image>태그로 변경
    • Remote image의 <Image> 사용법
      1. [설정] 이미지를 가져오는 API의 hostname 도메인을 추가하기
        • next.config.js 파일에서 구성
        • 기본형
          const nextConfig = {
            images: {
              remotePatterns: [
                {
                  protocol: 프로토콜,
                  hostname: 호스트명,
                  port: 포트,
                  pathname: 경로명,
                },
              ],
            },
          };
          module.exports = nextConfig;
          
        • ex.
          const nextConfig = {
            reactStrictMode: true,
            images: {
              remotePatterns: [
                {
                  protocol: "https",
                  hostname: "imagedelivery.net",
                  port: "",
                  pathname: "/<path_name>/**",
                },
              ],
            },
          };
          
      2. 이미지 크기를 명시하기
        • width, height 프로퍼티를 사용
          • lazy loading을 하기 위해 필요함
          • 실제 이미지 크기가 아닌, 보여지길 원하는 크기를 명시
        • 이미지의 크기를 제대로 모를 때 fill={true} 프로퍼티를 사용
          • 화면에 꽉 차는 absolute 이미지를 가지므로, relative 컨테이너와 함께 사용
          • object-fit CSS 프로퍼티와 함께 사용하는 것을 권장
            • 컨터이너의 내부 이미지가 보여지는 방식을 설정
          • ex.
            <div className="relative w-full h-96">
              <Image
                src={getImage(data.product.imageUrl, "public")}
                alt="product image"
                className="object-contain"
                fill={true}
              />
            </div>
            
        • 이미지 소스는 실제 이미지가 아닌, NextJS가 만든 이미지
    • remote image의 blur placeholder
      • blur placeholder는 local image에 대해서만 제공함
      • remote image 사용 시 blur image를 가지고 있다면, blurDataURL 프로퍼티를 사용
        • placeholder="blur"와 결합하여 사용한 경우에만 적용됨
        • base64로 인코딩된 이미지여야 함
        • 확대하여 흐려지므로, 10px 이하를 권장
          • 더 큰 이미지를 사용 시 App 성능이 저하될 수 있음
    • [Challenge] 채팅방
      • [prisma] model schema 작성하기
        • 채팅방, 채팅유저, 채팅
      • [Back-End] 채팅방 생성하기
        • 상품id, 판매자id, 구매희망자id를 통해 채팅방을 특정
        • 존재여부 확인 후, 채팅방으로 입장
          • 미 존재 시 채팅방 생성 후 입장
            • DB생성 : 채팅방, 채팅유저1, 채팅유저2
          • 존재 시 이미 존재하는 채팅방 입장
  • 24-02-23 : [Challenge] Chat system (2)
    • 개인방
      • 가장 최근 채팅을 10개 씩 pagination
        • 내림차순으로 정렬
        • 더보기 버튼을 클릭 시 데이터를 더 불러오고, 불러올 데이터가 없을 시 (가장 최근 불러온 데이타가 10개 미만) 버튼 숨김
      • 채팅 입력 시 mutate()를 사용해 즉시 UI에 보여줌
    • 채팅방 리스트
      • 가장 최신 채팅방을 위로 정렬
      • 각 채팅방의 최신 채팅과 시간을 미리 보여줌
        • timeago.js 패키지를 사용해 상대적인 시간을 표현
  • 24-02-24 : [Challenge] Chat system (3)
    • 채팅방 권한
      • 사용자가 URL에 직접 입력 시의 문제
        1. 해당 채팅방이 존재하는지
        2. 채팅방에 사용자가 포함되어 있는지
        • 채팅방 번호가 존재하고, 해당 채팅방에 본인이 포함되어 있는지 검사
      • 권한과 상대방의 이름을 같이 가져와서 사용
        • 미 권한 시 채팅방 리스트로 replace
  • 24-02-25 : [Challenge] Chat system (4)
    • 채팅방 목록에 대한 무한스크롤 pagination 구현
  • 24-02-27 : #19.0 ~ #19.5 / NextJS deep dive (1)
    • FIX
      • [community] timeago 시간 및 아바타 이미지 설정
      • [link-profile.tsx] 아바타 이미지 추가
    • Middlewares
      • 요청(request)과 종착지 중간에 있는 함수
        • API Route에서도 작동함
      • 사용법
        1. middleware 파일 생성하기
          • /middleware.ts 또는 /src/middleware.ts 파일을 생성
        2. middleware 작성하기
          • 기본형
            import type { NextRequest, NextFetchEvent } from "next/server";
            export function middleware(req: NextRequest, ev: NextFetchEvent) {
              ......
            }
            
      • NextJS의 middleware는 모든 요청에서 발생하지만, 페이지 하나를 불러올 때 마다 발생되는 관련 static파일 요청들이 있음
        • 매칭되는 URL에서만 middleware를 발생시키는 방법
          export const config = {
            matcher: ["/((?!api|_next/static|favicon.ico).*)"],
          };
          
      • 특정 페이지에서만 작동하는 방법
        • 기본형 : if (request.nextUrl.pathname.statsWith("경로")) { ... }
      • 사용자 기기정보
        • 기본형
          import { userAgent } from "next/server";
          const 변수명 = userAgent(요청변수);
          
        • 브라우저, 사용기기, 운영체제, CPU, Bot유무 등을 알 수 있음
        • ex.
          // Bot인 경우, 접속을 차단하고 해당 문구를 보여줌
          if (userAgent(req).isBot) {
            return new Response("No bot", { status: 403 });
          }
          
      • NextResponse 변수를 통해 .redirect(), .rewrite(), .json() 등 반환 가능
        • 현재 URL 정보를 복사(req.nextUrl.clone())한 후 사용해야 함
        • 항상 return문에서 사용할 것
      • 사용자의 쿠키를 확인하는 방법
        • 기본형 : req.cookie
        • 특정 쿠키를 가져오는 방법 : req.cookie.has(쿠키명) [Boolean]
        • 'iron-session'의 getIronSession()을 통해 세션쿠키를 가져올 수 있음
        • ex.
          // Check logged-in user using session
          const isCookie = req.cookie.has("carrot-session");
          const { user } = getIronSession<IIronSessionData>(
            req,
            NextResponse.next(), // res
            sessionOptions
          );
          const isUser = Boolean(isCookie && user);
          if (!isUser && !req.nextUrl.pathname.startsWith("/enter")) {
            const url = req.nextUrl.clone();
            url.pathname = "/enter";
            return NextResponse.rewrite(url);
          }
          
      • req.geo를 통해 사용자의 위치정보를 얻을 수 있음
        • 특정 지역을 차단하거나 허용 가능
      • 공식문서
    • Dynamic imports
      • import문을 최상단에서 선언한다면, 해당 컴포넌트를 즉시 사용여부와 상관없이 download 함
      • 즉시 사용하지 않는 컴포넌트는 사용할 때 download하여, App의 로딩시간을 최적화
        • NextJS의 Dynamic imports를 이용해 사용 가능
        • Lazy loading
      • 사용법
        import dynamic from "next/dynamic";
        const 컴포넌트명 = dynamic(() => import(경로), { ?옵션 });
        
        • 옵션
          • srr : 서버단에서의 로딩 유무를 설정
            • 몇몇 라이브러리나 패키지는 서바단에서 로딩하는 게 불가능 (에러)
          • loading : 로딩 중에는 보여지는 커스텀 컴포넌트
            • 사용법 : loading: () => 컴포넌트
          • suspense: true : React 18에서 지원하는 로딩 중 보여지는 커스텀 컴포넌트
            • 사용법
              <Suspense fallback={로딩 중에 보여줄 것}>
                <다이나믹컴포넌트 />
              </Suspense>
              
          • loading 또는 suspense(선호)를 사용
      • 주의 사항
        • 모든 컴포넌트를 dynamic 하지는 말 것 (App을 다 만든 후, 최적화 시 사용할 것)
        • 컴포넌트의 크기가 크고 사용자의 인터넷이 느리다면, 다운로드 시간이 많이 걸릴 것
    • 커스텀 document 컴포넌트 (_document.tsx)
      • NextJS App의 HTML 뼈대를 만드는 역할
        • 서버에서 한 번만 실행
        • _app.tsx는 App의 청사진이며, 사용자가 페이지를 불러올 때마다 브라우저에서 실행됨
      • <html />, 폰트, SEO 등 설정 가능
      • 파일명 : /src/_document.tsx
      • 기본형 (필수 컴포넌트)
        import { Html, Head, Main, NextScript } from "next/document";
        export default function Document() {
          return (
            <Html lang="ko">
              <Head />
              <body>
                <Main />
                <NextScript />
              </body>
            </Html>
          )
        }
        
        • <Main /> : App 컴포넌트를 rendering 함
      • 폰트 최적화 방법
        • 'google fonts'에서 원하는 폰트의 <link>를 <Head> 내에 입력하기
          • 'google fonts'에서 제공하는 폰트를 기반으로 NextJS 폰트 최적화가 가능
          • build 시 자동으로 최적화 (개발단계에서는 최적화 x)
            • 사용자가 다운받아야할 파일을 @font-face로 만들어 주므로, 사용자가 다운받을 필요 x
      • 외부 script를 추가하는 방법
        • 기본형: <Script src="경로" />
        • strategy 프로퍼티 : script를 불러올 타이밍을 정할 수 있음
          • beforeInteractive : 페이지를 다 불러와서 상호작용을 하기 전에 script를 불러옴
          • afterInteractive : [기본값] 페이지를 다 불러온 후, script를 불러옴 (대부분의 경우)
          • lazyOnLoad : 다른 모든 데이터나 소스들을 불러오고 나서야 script를 불러옴
        • onLoad 프로퍼티 : script를 불러온 후, 실행되는 콜백함수
      • 공식문서
  • 24-02-28 : #19.6 ~ #19.15 / NextJS deep dive (2)
    • getServerSideProps (SSR)
      • 페이지 컴포넌트가 서버단에서만 rendering 됨
        • getServerSideProps에서 반환된 데이터를 사용하여, 각 요청에서 이 페이지를 미리 rendering 함
      • 사용자의 요청이 발생할 때 마다 동작
      • 기본형
        export async function getServerSideProps(ctx: NextPageContext) {
          return {
            props: {}, // 페이지 컴포넌트에 props로 전송
          }
        }
        
      • 장점
        1. 로딩 상태를 보여주지 않음
        2. 소스코드 내부에 정보가 담김
        3. GET 요청 API handler를 굳이 만들지 않아도 됨 (DB 직접 사용 가능)
      • 단점
        • 페이지를 알아서 새로고침 해주는 패키지(SWR 등)를 사용 불가
          • static optimization 이나 cache 등을 사용 불가
        • error 시 사용자는 UI를 볼 수 없음
      • Date 타입 등을 읽지 못하는 경우, JSON.parse(JSON.stringify(변수명))을 사용해 반환
      • 공식문서
    • SSR + SWR 패키지
      • useSWR() 실행 전 미리 cache를 제공하여, cache가 있는 상태로 시작
        • client에서는 useSWR()을 그대로 사용 가능
      • export default인 컴포넌트로 getServerSideProps의 props가 전달 됨
      • 기본형
        function 페이지컴포넌트() { ... }
        export default function 컴포넌트명(SSR로부터받는props) {
          return (
            <SWRConfig
              value={{
                fallback: {
                  "경로1": 데이터값1,
                  "경로2": 데이터값2,
                },
              }}
            >
              <페이지컴포넌트 />
            </SWRConfig>
          )
        }
        export function getServerSideProps() { ... }
        
      • ex.
        function Home() {
          const { data } = useSWR<IProductList>("/api/products");
          ......
        }
        export default function Page({ data }: { data: IProductList }) {
          return (
            <SWRConfig
              value={{
                fallback: {
                  "/api/products": data,
                },
              }}
            >
              <Home />
            </SWRConfig>
          )
        }
        export async function getServerSideProps() {
          try {
            const products = await prismaClient.product.findMany({});
            return {
              props: {
                data: {
                  ok: true,
                  products: JSON.parse(JSON.stringify(products)),
                },
              },
            };
          } catch (error) {
            console.log(error);
            return {
              props: {
                data: {
                  ok: false,
                  error,
                },
              },
            };
          }
        }
        
      • 무한스크롤 참고용
    • SSR + Authentication
      • getServerSideProps()의 인자로부터 context 값을 가져올 수 있음
        • 타입 : NextPageContext
        • ex.
          import { getIronSession } from "iron-session";
          export async function getServerSideProps({ req, res }: NexPageContext) {
            const session = await getIronSession<제네릭>(
              req!,
              res!,
              세션옵션
            );
            ......
          }
          
      • 개인화된 사용자 데이터 또느 요청 시에만 알 수 있는 정보에 의존하는 페이지를 렌더링해야 하는 경우, getServerSideProps를 사용해야 함
        • ex. 승인 헤더 또는 위치 정보
      • 요청 시 데이터를 가져올 필요가 없거나 데이터와 미리 렌더링된 HTML을 캐시하기를 선호하는 경우, getStaticProps를 사용하느 것이 좋음
    • getStaticProps (SSG)
      • 정적(static) 웹사이트를 만들어주게 하는 기능
        • 정적(static) : 데이터가 바뀌지 않는 것 (API fetch x)
      • getStaticProps를 사용해야하는 상황
        • 페이지를 rendering하는 데 필요한 데이터는 사용자의 request보다 먼저 build 타임에서 이용가능해야 함
        • 데이터는 헤드리스 CMS에서 가져옴
        • 데이터를 공개적으로 cache 가능
        • 페이지는 SEO를 위해 미리 렌더링되어야 하고, 매우 빨라야 함
        • getStaticProps는 성능을 위해 CDN에서 cache할 수 있는 HTML 및 JSON 파일을 생성함
      • getServerSideProps는 사용자의 요청이 발생할 때 마다 동작하지만, getStaticProps는 딱 한 번만 실행됨
        • build 시 페이지를 export한 후 일반 HTML이 될 때
      • DB 또는 .md 파일 등을 이용해 정적 페이지 생성 가능
      • 기본형
        export async function getStaticProps(ctx: GetStaticPropsContext) {
          return {
            props: {},
          },
        }
        
    • gray-matter 패키지
      • .md파일의 front-matter를 파싱하는 패키지
      • front-matter : .md파일의 최상단에 위치한 메타데이터 블록
        • 기본형
          ---
          원하는키명: 원하는값
          ---
          
      • 설치법 : npm i gray-matter -D
      • 기본형
        import matter from "gray-matter";
        matter(파일변수, { ?옵션 });
        
      • 데이터 형태
        {
          content: 내용,
          data: {
            키명: 값,
          }
          isEmpty: 불리안값,
          excerpt: 값,
        }
        
      • getStaticProps와 함께 사용해 .md파일을 이용한 정적페이지를 생성 가능
        • ex.
          interface IBlogProps {
            posts: {
              title: string;
              date: string;
              category: string;
            }[];
          }
          export default function Blog({ posts }: IBlogProps) {
            return (
              <h1>Latest Posts :</h1>
              <ul>
                {posts.map((post, idx) => (
                  <li key={idx}>
                    <p>{post.title}</p>
                    <span>{post.date} / {post.category}</span>
                  </li>
                ))}
              </ul>
            );
          }
          export async function getStaticProps() {
            // normal node.js
            const blogPosts = readdirSync("src/posts").map((file) => {
              const content = readFileSync(`src/posts/${file}`, "utf-8");
              return matter(content).data;
            });
            return {
              props: {
                posts: blogPosts,
              },
            };
          }
          
      • 공식문서
    • getStaticPaths
      • 정적 경로(static path)를 생성하기 위해 사용되는 메서드
        • 동적인 URL을 갖는 페이지에서 getStaticProps를 사용할 때 필요함
      • dynamic params는 동적인 페이지라서 변수의 값이 무한하지만, 정적인 페이지에서는 따로 유한한 값(몇 개의 페이지를 갖는지)을 명시해야 함
        • 미리 HTML 페이지를 생성해야하기 때문
      • 기본형
        export function getStaticPaths() {
          return {
            paths: 배열값,
            fallback: 불리안값,
          };
        }
        
        • paths : { params: { 동적변수: 값 } }[] 형태이어야 함
        • fallback : 미리 build된 페이지가 없는 경우, NextJS가 동적 페이지를 생성하는 방법을 제어하는 옵션
          • false : 미리 정의된 경로 이외의 요청은 404 페이지를 반환
          • true : 미리 정의된 경로 이외의 쵸엉에 대해 동적으로 페이지를 생성하고, 해당 페이지를 캐싱함
            • 사용자가 요청한 페이지에 대해 동적으로 생성된 페이지를 제공 가능
          • "blocking" : 미리 정의된 경로 이외의 요청에 대해 동적으로 페이지를 생성하지만, 요청이 발생할 때까지 build를 차단함
            • 장점 : 요청에 대한 응답을 기다리지 않고 미리 정의된 경로에 대한 페이지를 build할 수 있음
            • 단점: 많은 양의 동적 페이지가 있는 경우 서버 부하를 초래함
      • getStaticProps()contextgetStaticPaths()가 return하는 각각의 path들을 호출함
        • 기본형 : ctx.params?.동적변수명
    • remark-html 패키지
      • Markdown을 파싱하고 HTML로 변환하는 패키지
      • 설치법 : npm i remark-html remark-parse unified
      • 기본형
        import remarkHtml from "remark-html";
        import remarkParse from "remark-parse";
        import { unified } from "unified";
        const 파일변수 = await unified()
          .use(remarkParse)
          .use(remarkHtml)
          .process(md파일내용);
        
      • ex.
        // [slug].tsx
        interface IPostProps {
          post: string;
        }
        export default function Post({ post }: IPostProps) {
          return <div>{post}</div>;
        }
        export function getStaticPaths() {
          const files = readdirSync("src/posts").map((file) => {
            const [name, _] = file.split(".");
            return { params: { slug: name } };
          });
          return {
            paths: files,
            fallback: false,
          };
        }
        export async function getStaticProps(ctx: GetStaticPropsContext) {
          const { content } = matter.read(`src/posts/${ctx.params?.slug}.md`);
          const { value } = await unified()
            .use(remarkParse)
            .use(remarkHtml)
            .process(content);
          return {
            props: {
              post: value,
            },
          };
        }
        
      • 공식문서
    • InnerHTML
      • React가 자동으로 텍스트가 HTML 코드라면 실행시키지 않음
      • 신뢰할 수 있는 데이터(변수)를 HTML로 바꾸는 방법
        • 요소에서 dangerouslySetInnerHTML={{ __html: 변수 }}
        • style 적용이 안 되어있기 때문에 CSS 스타일링을 해야함
      • 일반 CSS에서 Tailwind를 사용하는 방법
        • 기본형 : 선택자 { @apply Tailwind문법 }
        • ex.
          .blog-post-content h1 {
            @apply mb-5 text-red-500;
          }
          <div className="blog-post-content" />
          
  • 24-03-01 : #20.0 ~ #20.7 / ISR (Incremental Static Regeneration)
    • ISR (Incremental Static Regeneration; 단계적 정적 재생성)
      • 데이터가 포함된 페이지를 불러올 때 CSR 또는 SSR 방식을 택해야 함
      • ISR 방식은 getStaticProps처럼 페이지를 미리 정적인 상태로 변환시키나, build 시에만 적용되는 단점을 개선한 방식
        • 정적 페이지를 백그라운드에서 개별적으로 몇 번이고 다시 생성시킬 수 있음
        • Front단에서 ReactJS코드(fetch)가 필요하지 x
      • 장점
        1. 페이지 로딩 상태가 전혀 나타나지 않음
        2. 서버단에서 페이지를 rendering하지 않아도 됨
          • 사용자가 요청할 때 마다 서버단에서 실행 x
        3. 표시되는 데이터는 가장 최신 데이터
          • 사실은 백그라운드에서 생성한 캐싱된 HTML 페이지를 표시
      • 기본형
        export async function getStaticProps() {
          return {
            props: {},
            revalidate: 초단위,
          }
        }
        
        • revalidate : [초단위] 정적 페이지의 재생성 주기
      • 사용자가 아닌 웹사이트가 기준이며, 주기적으로 데이터를 업데이트하여 static 페이지를 제공
      • 일반적으로 ISR이 SSR 보다 DB를 적게 사용함
        • ISR이 캐싱된 정적 페이지를 제공하는 동안 DB에 접근할 필요가 없기 때문
      • 공식문서
    • ODR (On-Demand Revalidation)
      • 수동(사용자 요청)으로 getStaticProps를 API handler로 작동 가능
        • 모든 페이지를 HTML로 제작 가능
      • 기존의 ISR에서 정적 페이지를 재생성할 수 있는 방법은 사용자가 revalidate 시간 이후에 페이지를 방문하는 것
      • 기본형 : API handler에서 await res.revalidate(URL경로)
        • ODR 사용 시 ISR의 revalidate는 사용하지 않아도 됨
      • 무분별하게 ODR을 실행하는 것을 막기위해 ODR API에 비밀 token을 추가하여 사용할 것
        • rewrites()를 사용해 Front단에서 token을 가림
      • 공식문서
    • 동적 경로(dynamic params)에서의 ISR 사용
      • 미리 알 수 없는 params에 대해 정적 페이지를 설정하여 사용
      • 사용법
        1. 동적 페이지를 정적 페이지(getStaticProps)로 바꾸기
        2. 정적 경로(getStaticPaths) 설정하기
          • build 시 모든 DB의 데이터를 가져오는 것은 좋은 방법이 아님
          • getStaticPaths에서 빈 배열의 경로들을 반환하고, 사용자의 요청에 따라 미리 만들도록 함
          • 기본형
            export function getStaticPaths() {
              return {
                paths: [],
                fallback: "blocking",
              }
            }
            
            • fallback: "blocking" : getStaticPaths를 가지는 페이지에 방문 시 해당 HTML파일이 없다면, 사용자를 잠시 기다리게 한 뒤 백그라운드에서 페이지를 생성한 후 보여줌
              • 서버단에서 실행되며, 첫 실행 시 딱 한 번만 일어남
            • fallback: true : "blocking"처럼 작동하지만, 로딩 중에도 화면을 보여줌
              • useRouter().isFallback 조건문을 통해 로딩 중 화면을 보여줄 수 있음
  • 24-03-04 : [Challenge] ISR (Incremental Static Regeneration)
    • [Challenge] /community/[id].tsx를 정적 페이지로 만들기
      • getStaticProps(SSG)를 사용해 대부분의 데이터를 가져옴
        • 궁금해요, 댓글 데이터 변경 시 await res.revalidate(경로)를 실행하여, 정적 페이지 갱신
        • 동적 URL에 대해 getStaticPaths 사용
          export const getStaticPaths: GetStaticPaths = () => {
            return {
              paths: [],
              fallback: "blocking",
            };
          };
          
      • isWondering 부분은 사용자(session)마다 값이 다르므로, 이것만 CSR 방식으로 가져옴
        • 정적 페이지에서는 req, res, session 등 사용 불가
      • mutate
        • 궁금해요 : useSWR()mutate() 사용
        • 댓글 : 댓글배열.push()
      • ISSUE : 상대시간(timeago.js 등)에 대해 client와 server간 콘텐츠 불일치 에러 발생
        • FIX : <time dateTime={시간값} suppressHydrationWarning>값</time>
        • ex.
          <span className="text-xs font-normal text-gray-500 block">
            <time dateTime={answer.createdAt} suppressHydrationWarning>
              {formatTime(answer.createdAt, true)}
            </time>
          </span>
          
        • 공식문서
      • 궁금해요, 댓글 등 상호작용 요소가 많기 때문에 정적페이지 사용이 별로인 것 같음
        • 오히려 DB 사용량이 더 많은 것 같음
          • 요청 시 마다 '궁금해요' DB를 사용함
          • 댓글, 궁금해요 POST 시 DB 사용 및 res.revalidate()
        • SSR 방식을 사용하기로 결정
  • 24-03-06 : #21.0 ~ #21.5 / React 18
    • FIX
      • [profile/edit] 아바타 이미지를 교체하지 않을 시 error 수정
    • UPDATE
      • [/community/[id].tsx]를 SSR 페이지로 만들기
      • SSR + SWR인 경우, error 페이지 핸들링
      • [enter] token 로직 업데이트 1. 해당 사용자의 모든 token을 삭제 후, 새로운 토큰을 생성 2. 사용자의 method(email, phone)와 난수가 일치 시 로그인
        • 난수를 unique로 할 필요 없음 (method가 unique값이기 때문)
    • suspense
      • 코드에서 loading 상태를 나타내는 부분을 제거할 수 있게 해주는 API
        • loading 상태에 대해 신경쓰지 않아도, 사용자가 loading 상태 화면을 볼 수 있음
      • 주의사항
        • SSR, SSG 등과 함께 사용 불가 (CSR에서만 사용 가능)
        • 개발자가 사용한다고 되는 게 아니라, 라이브러리에서 기능을 지원해줘야 함
      • 장점 : 이미 데이터가 있다고 가정 하에 코드를 짤 수 있음
        • if문을 사용해 데이터를 체크할 필요가 없음
      • (SWR 사용 시) 페이지 전체인 경우
        • 기본형
          import dynamic from "next/dynamic";
          function 화면컴포넌트() {
            <SWRConfig value={{ suspense: true }}>
              <Suspense fallback={로딩컴포넌트}>
                <페이지컴포넌트 />
              </Suspense>
            </SWRConfig>
          }
          export default dynamic(async () => 화면컴포넌트, { ssr: false });
          
        • 모든 SWR 요청이 끝날 때 까지 기다린 후, 컴포넌트를 렌더링 해줌
      • (SWR 사용 시) 컴포넌트인 경우
        • 페이지에서 useSWR()를 사용하면 안 됨
        • 기본형
          const 컴포넌트명 = () => {
            const { data, isLoading } = useSWR(경로);
            return ......
          }
          export default function 페이지컴포넌트명() {
            return (
              <Suspense fallback={로딩컴포넌트}>
                <컴포넌트명 />
              </Suspense>
            )
          }
          
    • Server Components (RSC; React Server Components)
      • 서버 컴포넌트는 client에서 JavaScript를 전혀 사용하지 않아도 생성 가능
        • 사용자가 JavaScript를 로딩하지 않아도 됨 ➡️ 빠름, SSR은 아님
        • 서버가 컴포넌트들을 rendering 한 후, 사용자에게 결과물만 보여줌
          • 컴포넌트에서 서버관련 일(DB 등)을 할 수 있을 것
      • NextJS의 App Router에서는 기본적으로 Server Components를 사용함
        • 필요 시 Client Components를 사용 가능
      • 설치법 : npm i next@latest react@rc react-dom@rc
      • 설정법 : 'next.config.js'파일에서 experimental 프로퍼티 추가하기
        module.exports= {
          experimental: {
            reactRoot: true,
            runtime: "nodejs",
            serverComponents: true,
          },
        }
        
      • 사용법
        1. 파일명.server.tsx로 파일명 변경하기
        2. Suspense를 사용해 결과물만 화면에 반환하기
      • 공식문서
  • 24-03-07
    • UPDATE
      • [token] 토큰의 유효기간으로 3분 설정
      • [/products/[id]] SSR + SWR로 변경
      • [404] 동적URL에서 data가 없을 시 404 반환
  • 24-03-08
    • UPDATE
      • [form] useForm register의 검증 옵션 및 error 메시지 추가
      • [/user/profile/[id]] 사용자 프로필 페이지 생성 (SSG)
      • [user/edit] 프로필 이미지 갱신 시 cloudflare에서 기존 이미지 삭제
        • 기본형
          await fetch(
            `https://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/images/v1/<IMAGE_ID>`,
            {
              method: "DELETE",
              headers: {
                "Content-Type": "application/json",
                Authorization: `Bearer <API_TOKEN>`,
              },
            }
          );
          
        • 공식문서
      • [product/[id]] 수정/삭제 기능 추가
      • favicon
    • FIX
      • [chats/[id]] 프로필 링크 수정
  • 24-03-09
    • UPDATE
      • [Font] 폰트 적용
        • next/font는 모든 글꼴 파일에 자동적으로 자체 호스팅이 내장되어 있음
        • 레이아웃 쉬프트 없이 폰트를 사용 가능
        • 구글 폰트에 요청을 보내지 않음
        • 기본형
          import { 폰트명 } from "next/font/google";
          const 변수명 = 폰트명({
            subsets: ["latin"],
            display: "swap",
          });
          <요소 className={변수명.className} />
          
        • 공식문서
        • 참고자료
      • [product/edit] 프로필 이미지 갱신 시 cloudflare에서 기존 이미지 삭제
      • [SEO] 모든 페이지에 <title> 적기
      • [/api/users/me/index.ts] POST부분 리팩토링
        • 업데이트할 컬럼들을 한 객체에 담은 후, 한 번에 업데이트
      • [무한스크롤] 홈(SSR), 동네생활(SSR), 채팅, 라이브(SSR), 판매내역(SSR), 구매내역(SSR), 관심목록(SSR)
        • useSWRInfinite()의 옵션
          • revalidateFirstPage : [boolean] 첫페이지 재검증 여부 (기본값 true)
          • fallbackData : [T[] | undefined] 첫페이지의 초기데이터
        • ex.
          const { data, setSize, isLoading, isValidating } = useSWRInfinite<T>(getKey함수, {
            revalidateFirstPage: false,
            fallbackData: 초기데이터 ? [초기데이터] : undefined,
          });
          
  • 24-03-10
    • UPDATE
      • [community/[id]] 댓글 pagination
    • FIX
      • [chats/[id]] pagination 이벤트 버튼 활성화 조건 수정
  • 24-03-11
    • UPDATE
      • [채팅방] 채팅방 삭제 및 물건 post 삭제 기능 구현하기
        • 구매자 or 판매자가 대화 종료 시
          • 채팅방 관련만 삭제
    • TODO
      • [채팅방] 구매자가 구매 완료 시
        • 채팅방, 물건 관련 전부 삭제하기
        • 판매내역(판매자), 구매내역(구매자) 추가하기
        • ?? product를 삭제하면 cascade때문에 다 삭제됨
          • isSoldOut 같은 boolean 값을 추가해야 하나?
  • 24-03-14
    • UPDATE
      • [DB] 'records' 로직 다시 및 구현하기 (구매내역, 판매내역)
        • 삭제 시 cascade를 하지 않 새로운 model 생성하여 적용
    • TODO
      • [product] 판매종료내역 관련
  • 24-03-15
    • UPDATE: [chats/[id]]
      • 현재 거래중인 물품의 link 추가
      • 물건 구매 확정 시 review 기능(옵션) 업데이트
    • ISSUE
      • [api/products/[id]] product "DELETE" 시 product와 record 간의 관계 때문에 에러
        • record와 product 간의 관계를 끊어내고, 일일이 저장하는 방식으로 해야하나? (model 수정)
  • 24-03-16
    • FIX
      • [chats/[id]] 구매 확정 시 product 이미지 삭제 (CloudFlare)
      • [DB] Record 모델과 Product 모델 간의 관계를 끊음
  • 24-03-18 : #22.0 ~ #22.6 / Deploy
    • [middleware] 특정 지역 차단하기
      • req.geo를 사용
      • 호스팅 시 데이터를 제공받을 수 있음
  • 24-03-23 : Open graph
    • crawler-user-agents 패키지
      • 단일 JSON 파일처럼 로봇, 크롤러 및 스파이더가 사용하는 HTTP 사용자 에이전트 목록이 포함되어있는 패키지
      • 설치법 : npm i crawler-user-agents -D
      • 사용법
        import crawlers from "crawler-user-agents";
        if (RegExp(entry.pattern).test(req.headers['user-agent'])) { ... }
        
      • ex.
        // Defend from unknown bot
        const userAgentInfo = userAgent(req);
        if (
          userAgentInfo.isBot &&
          !crawlers.some((entry) => RegExp(entry.pattern).test(userAgentInfo.ua))
        ) {
          return new Response("No bot", { status: 403 });
        }
        

  • To-Do
    • [Token] 한 토큰에 대해 여러 번 시도하는 것을 막기
    • [/enter] 소셜 로그인 구현하기 (NextAuth)
    • 로그아웃 구현하기

About

NextJS, Tailwind, Prisma, PlanetScale, Cloudflare를 사용해 serverless '당근마켓'을 클론코딩합니다. Serverless 'CARROT MARKET' clone coding using NextJS, Tailwind, Prisma, PlanetScale, Cloudflare.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Languages