NOTE

[Next + React Query로 SNS 서비스 만들기] 섹션 4 정리

ssund 2024. 9. 12. 17:10

섹션4

백엔드 서버 세팅하기

백앤드 환경설정 

- node 설치

- postgresql 설치

 

설치방법
postgres라는 db 관리자가 생긴다.

 

비밀번호 설정해준다. 

 

포트는 5432로 설정한다.

 

- redis 설치 (로그인 할 때 사용)
https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/install-redis-on-mac-os/

 

- Nest-prisma 세팅

https://github.com/zerocho/nest-prisma 폴더다운로드 해서 사용한다. 

npm install 로 패키지 설치하기 

.env 파일 수정

DATABASE_URL="postgresql://postgres:비밀번호입력@localhost:5432/zcom?schema=public"

 

pgAdmin 4 실행

1. register server  (name 입력, connection hostname, port 입력, 비밀번호 입력)

 

2. create database

zcom을 만들어준다.

 

3. nest-prisma 저장소로 돌아가 명령어로 migrate하기

npx prisma migrate dev

 

4. pgadmin에서 schemas/public/tables 경로에 데이터 들어와있는지 확인하기

nest 서버 실행하기

npm run start:dev

 

로컬 9090포트에서 서버확인, swagger 확인

http://localhost:9090/

http://localhost:9090/api

 

로그인과 회원가입 실제로 하기

서버액션은 오류가 나더라도 클라이언트에서 오류로 나오지 않기때문에 확인하기 힘들다.

회원가입시 fetch 옵션 credentials: 'include'를 넣어줘야 세션 쿠키가 브라우저에 전달, 등록된다.

const response = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/users`, {
  method: 'post',
  body: formData,
  credentials: 'include',
})

 

회원가입을 하면 db에 사용자에 데이터가 들어간것을 확인할수있다.

 

주소를 rewrite 해야하는경우 next.config.js에서 설정할 수있다.

const nextConfig = {
  async rewrites() {
    return [
      {
        source: '/upload/:slug',
        destination: 'http://localhost:9090/upload/:slug',
      },
    ]
  },
}

module.exports = nextConfig

 

업로드 이미지 미리보기

react-textarea-autosize

textarea 텍스트 사이즈에 맞춰 자동 크기 조절 되는 플러그인

 

이미지 미리보기 

readAsDataURL은 파일 컨텐츠 데이터를 읽어오는 역할을 한다. read 행위가 종료되면 onloadend 이벤트가 트리거 된다. 
base64 인코딩 된 스트링 데이터가 result에 담아지게 된다.

 

참고 
https://developer.mozilla.org/ko/docs/Web/API/FileReader/readAsDataURL

 

Array.from 메소드

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/from

const onUpload = (e: any) => {
  e.preventDefault();

  if (e.target.files) {
    Array.from(e.target.files).forEach((file: File, index: number) => {
      const reader = new FileReader();

      reader.onloadend = () => {
        setPreview((prevPreview: any) => {
          const prev = [...prevPreview];
          prev[index] = {dataUrl: reader.result as string, file};
          return prev

          //return [...prevPreview, reader.result as string]
        })
      }

      reader.readAsDataURL(file); //
    })
  }
}

 

세션쿠키 공유하기 & 게시글 업로드 완성

프론트는 로그인시 세션쿠키를 사용하지만 백엔드는 세션쿠키를 사용하지 않는다.

session-token(클라이언트용), connect.sid (백엔드용)

 

클라이언트에서는 next-auth 기능을 사용하면 되고, 서버에서는 auth.ts에서 export한 메서드를 사용하면 된다.

Auth.ts에서 로그인시 connect.sid 저장하는 로직을 추가해준다.

서버에는 로그인값을 저장못한다. 다른사람들도 함께 사용하는 공용의 서버이기때문에

providers: [
    CredentialsProvider({
      async authorize(credentials) {
        const authResponse = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/login`, {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify({
            id: credentials.username,
            password: credentials.password,
          }),
        })
        let setCookie = authResponse.headers.get('Set-Cookie');
        console.log('set-cookie', setCookie);
        if (setCookie) {
          const parsed = cookie.parse(setCookie);
          cookies().set('connect.sid', parsed['connect.sid'], parsed); // 브라우저에 쿠키를 심어주는 것
        }
        if (!authResponse.ok) {
          return null
        }

        const user = await authResponse.json()
        console.log('user', user);
        return {
          email: user.id,
          name: user.nickname,
          image: user.image,
          ...user,
        }
      },
    }),
  ]

 

useMutation 사용하기

react useMutation 사용하는 이유는 상태관리가 가능하다(ispending, isError, isSuccess)

optimistic update를 활용할수있다. (게시글올렸을때 바로성공했다고 치고 화면에 올려준다.)

좋아요 누르기 정도 도입해서 빠른 상호작용을 보여주면 좋을듯 하다.

onMutate() {
    return '1111'
},
onSuccess(response, variable, context) 
// context는 onMutate에서 return한 값

onSettled() { // 마지막에 무조건실행한다. 
    queryClient.invalidateQueries({
        queryKey: ['posts'] // invalidateQueries는 posts를 쿼리키로 가진 애들을 다시 가져오는 메소드
    })
}

 

데이터를 안불러온상태 pending

데이터를 불러오고 있는상태 fetching

 

참고
https://tanstack.com/query/latest/docs/framework/react/reference/useMutation

 

주소에 해시가 들어가면 문제가 됩니다.

주소에 특수문자 해시가 들어가면 서버에 정보가 넘어가지 않는다. encoding을 해줘야한다. 

encodeURIComponent('#test')

 

api에서도 쿼리스트링 url객체로 변경해서 toString 해줘야한다.

`http://localhost:9090/api/post?${urlSearchParams.toString()}`

 

하트 누를때 optimistic update 적용하기

팔로우 , 하트누를 때 적용

복잡한 구조에서 해당id를 찾고 불변성을 지키면서 업데이트를 해줘야하는경우 로직이 복잡해 질수있다. 

immer는 불변성을 지키면서 값을 변경할 수 있도록 해주는 라이브러리 

https://react.vlpt.us/basic/23-immer.html

 

Array.flat()

여러차원의 배열을 1차원 배열로 만들어주는 메소드

참고 https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/flat

// immer 사용한 경우 
shallow.pages[pageIndex][index].Hearts = [{userId: sesson?.user?.email as string}];
shallow.pages[pageIndex][index]._count.Hearts += 1;
// immer 사용하지 않는경우
const pageIndex = value.pages.findIndex((page) => page.includes(obj));
const index = value.pages[pageIndex].findIndex((v) => v.postId === postId);
console.log('found index', index);
const shallow = { ...value };
value.pages = {...value.pages }
value.pages[pageIndex] = [...value.pages[pageIndex]];
shallow.pages[pageIndex][index] = {
  ...shallow.pages[pageIndex][index],
  Hearts: shallow.pages[pageIndex][index].Hearts.filter((v) => v.userId !== session?.user?.email),
  _count: {
    ...shallow.pages[pageIndex][index]._count,
    Hearts: shallow.pages[pageIndex][index]._count.Hearts - 1,
  }
}

 

 

서로 다른 컴포넌트간 query 일치하게 하기

onError()

첫번째 인자가 에러 정보

두번째인자는 받는 매개변수

 

완전히 로그아웃하기 , 프론트서버에 쿠키보내기

react query invalidateQueries를 사용하여 캐시를 날려준다.

queryClient.invalidateQueries({
        queryKey: ['posts'] // invalidateQueries는 posts를 쿼리키로 가진 애들을 다시 가져오는 메소드
    })

 

fetch를 이용해 로그아웃 api을 호출하면 session이 날라간다.

prefetch query로 getuser가 서버에서 실행될때는

fetch함수에 credential: includes를 넣어도 브라우저 쿠키를 가져오진 못한다

직접 cookie 를 header에 넣어주면된다.

import {cookies} from 'next/headers'

headers: {Cookie: cookies().toString()}

 

메타데이터 설정하기

서버컴포넌트에서 실행된다.

동적으로 메타데이터 생성할때는 generateMetadata를 사용하면 된다.

layout.tsx, page.tsx 에서 설정하면 된다. 

export async function generateMetadata({searchParam}) {
    return {
        title: `${searchParam.q}`
        description: `${searchParam.q}`
    }
}


동적으로 생성하지 않아도 되는경우 metadata로 사용한다,

export const Metadata = {
    title: '홈',
    description: '설명'
}


SSR 적용기준

서버사이드 컴포넌트 확인 하는 법 

네트워크 탭 에서 미리보기하면 SSR인지 확인할수있다. 

 

SSR하면 좋은 페이지 - 공유하기가 많은 페이지, 로그인 없이 접근가능한 페이지

서버사이드 장점 - 검색엔진에 최적화

서버사이드 단점 - 프론트서버에 부하가 갈수있다.

 

zustand 사용하기

zustand create를 이용하여 store를 만들수있다. 

store 값을 변경하는 set action을 만들어 불변성을 지키면서 변경할 수 있다. 

import { Post } from "@/model/Post";
import { create } from "zustand";

interface ModalState {
  mode: "new" | "comment";
  data: Post | null;
  setMode(mode: "new" | "comment"): void;
  setData(data: Post): void;
  reset(): void;
}

export const useModalStore = create<ModalState>((set, get) => ({
  // 초기 데이터
  mode: "new",
  data: null,

  setMode(mode) {
    set({ mode });
  },
  setData(data) {
    set({ data });
  },
  reset() {
    set({
      mode: "new",
      data: null,
    });
  },
}));