NextJS/NextJS - ToDoList

[NextJs] - 풀 스택 투두리스트 만들기 (마지막) (feat. SSR, Generic, Api Router)

위르겐 2023. 3. 2. 23:45

모든 준비는 끝났다!

투두리스트 지금 바로 시작~~~합니다.

 

 

pages폴더 안에 있는 index.tsx파일에

h1, input, textarea, button 태그들을 이용해

위 모양처럼 만들어주자.

( html과 css는 자유롭게 ^^ ) 

 

import { GetServerSideProps } from 'next';
import { ChangeEvent, useEffect, useState } from 'react';
import { prisma } from '../lib/prisma';
import axios from 'axios';
import { useRouter } from 'next/router';

export default function Home() {

  return (
    <div>
      <h1>To-Do-List</h1>
      <form
        onSubmit={(e) => {
          e.preventDefault();
          createNote(form);
        }}
      >
        <input
          type='text'
          placeholder='Title'
          name='title'
          value={form.title}
          onChange={onChange}
        />
        <textarea
          placeholder='Content'
          name='content'
          value={form.content}
          onChange={onChange}
        />
        <button type='submit'>
          Add +
        </button>
      </form>
    </div>
  );
}

뼈대는 이런식으로 잡히게 될텐데

태그 안의 속성은 아래에서 설명할 예정이다.

 

 

return문 가장 아래에 

정확히는 Home컴포넌트를 벗어난 위치에서

export const getServerSideProps: GetServerSideProps = async () => {};

getServerSideProps 함수를 만들어 주자.

 

getServerSideProps는

서버사이드렌더링 방식이며

클라이언트가 서버에 매번 데이터를 요청해 서버에서 처리하는 방식인데

클라이언트 요청이 들어올 때마다 매번 서버에서

새로운 화면을 만들어 제공해주는 것이다.

 

우리가 nextJS를 사용하는 이유이다.

 

그동안 리액트를 사용했을 땐

특별한 조치를 취하지 않는 한

 

모든 페이지를 

클라이언트인 브라우저가 직접

렌더링을 했었다.

 

즉 서버에서 받은 데이터를 통해

클라이언트인 브라우저가 화면을 그리는 주체였던 것이다.

 

 

https://jurgen-94.tistory.com/89

 

SSR(Server Side Rendering)

리액트와 Next.js에 대해 공부하면서 CSR과 SSR에 대해 자연스럽게 헷갈리게 됐고(?ㅋㅋ) 찾아보았다. 공부한 내용을 간략히 설명하자면 CSR과 SSR의 정의 CSR은 클라이언트 사이드 렌더링의 약어로 프

jurgen-94.tistory.com

필자가 위 글에서 

서버 사이드 렌더링의 장단점을 기술해놓았다.

 

서버 사이드 렌더링의 장점을 이용하기 위해

우리는 nextJS를 사용하는 것이다.

( csr, ssr과는 또 다른 방식인 ssg는 천천히 알아보자 )

 

그럼

 

getServerSideProps함수에서 

DB의 데이터를 꺼내어

컴포넌트에 props로 전달해보자.

 

export const getServerSideProps: GetServerSideProps = async () => {
  const notes = await prisma.note.findMany({
    select: {
      title: true,
      id: true,
      content: true,
    },
  });
  notes.sort((a, b) => {
    return a.id < b.id ? -1 : 1;
  });
  return {
    props: {
      notes,
    },
  };
};

 

현재 연결된 DB의 

note라는 스키마안에 있는 모든 데이터를 가져올 것이다.

 

findMany는 모든 note들을 찾을 것이고

브라우저에 표시된 그 note들은

title, id, content의 키값만을 가지게 될 것이다.

 

(원래는 createdAt과 updatedAt도 있었다)

 

나는 해당 notes들을 id순으로 정렬하고 싶기 때문에

sort정렬을 이용했고

 

props: { notes: notes }를 반환해준다.

 

그럼 이제 이 반환된 notes들을

Home 컴포넌트에서 props로 받아와줘야 한다

 

import { GetServerSideProps } from 'next';
import { ChangeEvent, useEffect, useState } from 'react';
import { prisma } from '../lib/prisma';
import axios from 'axios';
import { useRouter } from 'next/router';

interface FormData {
  title: string;
  content: string;
  id: string;
}

export default function Home({ notes }: { notes: FormData[] }) {
  return (
  (상단에 있는 html코드들)
  );
}

 

props를 받을 땐 타입을 지정해줘야하기 때문에

select로 선택된 키값들의 타입을 

interface로 선언해주고

props에 그 타입을 지정해준다.

 

그럼 우리가 리액트에서 늘 해왔던것 처럼

컴포넌트가 notes props를

사용하여 뷰를 그릴 수 있게 되는 것이다...!!

 

다만 차이점은

이 컴포넌트는 서버에서 직접 그린 후에

브라우저에 보여진다는 것

 

정말 큰 차이다.

 

 

 

 

 

 

이제 데이터를

CRUD할건데 

기존의 리액트를 하셨던분들이라면

아주 쉽게 이해하실 수 있으리라 생각한다.

 

export default function Home({ notes }: { notes: FormData[] }) {
  const router = useRouter();
  const baseUrl = 'http://localhost:3000/api';
  const [form, setForm] = useState<FormData>({
    title: '',
    content: '',
    id: '',
  });

  const onChange = <T extends HTMLInputElement | HTMLTextAreaElement>(
    e: ChangeEvent<T>
  ): void => {
    setForm({
      ...form,
      [e.target.name]: e.target.value,
    });
  };

  const refreshData = () => {
    setForm({ title: '', content: '', id: '' });
    router.replace(router.asPath);
  };

  async function createNote(data: FormData) {
    if (data.id) {
      await axios.put(`${baseUrl}/note/${data.id}`, { data }).then(() => {
        refreshData();
      });
      return;
    }
    await axios.post(`${baseUrl}/create`, { data }).then(() => {
      refreshData();
    });
  }

  async function deleteNote(id: string) {
    await axios.delete(`${baseUrl}/note/${id}`).then(() => {
      refreshData();
    });
  }
  return (
  	상단의 html코드
  )
}

맨 위부터

하나씩 차근차근 살펴보자

 

  const router = useRouter();
  const baseUrl = 'http://localhost:3000/api';
  const [form, setForm] = useState<FormData>({
    title: '',
    content: '',
    id: '',
  });

 

next의 router훅을 사용하기 위해 router변수 선언해주고

baseUrl은 추후 만들어 둘 http://localhost:3000/api 로 선언해준다.

useState 훅을 이용해 

title과 content 그리고 id의 키값을 가진

객체를 선언해주자.

 

  const onChange = <T extends HTMLInputElement | HTMLTextAreaElement>(
    e: ChangeEvent<T>
  ): void => {
    setForm({
      ...form,
      [e.target.name]: e.target.value,
    });
  };

 

우리는 타입스크립트를 사용하기 때문에

onChange함수에 제네릭을 넣어줄 것이다.

 

우리는 input태그와 textarea태그를 사용하고 있으니

제네릭인 <T>에 

HTMLInputElement와 HTMLTextAreaElement

로 제한을 걸어준 후

이벤트 파라미터의 타입을

e: ChangeEvent<T>

이렇게 지정해준다.

 

그 아래 나머지는

input태그의 name속성을 이용해

title의 name을 가진 input태그에 입력했을 때

title의 value가 바뀔것이고

다른 input (여기서는 textarea)

을 건드릴 때도 해당 name의 value만 바뀔것이다.

 

  const refreshData = () => {
    setForm({ title: '', content: '', id: '' });
    router.replace(router.asPath);
  };

 

위 함수는

data가 create되거나 update 혹은 delete되었을 때

자동으로 리프레시 해주기 위함인데

우선 이벤트가일어나면 input태그들의 value를 전부 빈값으로 비워주고

router.asPath를 이용하여

데이터를 다시불러온다.

 

asPath는 정확히

 - basePath이나 locale이 포함되지 않은 path
    ex ) 'https://localhost:3000/501da229-9915-4264-9d37-6783545ee553' 의 경우

            asPath = '/501da229-9915-4264-9d37-6783545ee553'

 

위의 의미를 뜻한다.

 

여기서는 localhost:3000 뒤에 아무것도 없기때문에

새로고침의 효과가 되는 것이다.

 

  async function createNote(data: FormData) {
    if (data.id) {
      await axios.put(`${baseUrl}/note/${data.id}`, { data }).then(() => {
        refreshData();
      });
      return;
    }
    await axios.post(`${baseUrl}/create`, { data }).then(() => {
      refreshData();
    });
  }

  async function deleteNote(id: string) {
    await axios.delete(`${baseUrl}/note/${id}`).then(() => {
      refreshData();
    });
  }

 

createNote와 deleteNote

함수만을 이용해

CRUD를 전부 구현할 수 있다.

 

나는 fetch보다 axios를 선호하기 때문에

axios를 이용할 것이다.

 

 

createNote는 Add +

버튼을 누를 시 실행되는 함수이다.

  async function createNote(data: FormData) {
    if (data.id) {
      await axios.put(`${baseUrl}/note/${data.id}`, { data }).then(() => {
        refreshData();
      });
      return;
    }
    await axios.post(`${baseUrl}/create`, { data }).then(() => {
      refreshData();
    });
  }

 

data.id가 있다면 put 메소드 요청을 보낼건데

update하는 방식이 

투두리스트 아이템의 update버튼을 누를 시 

해당 title content id가 

input과 textarea에 채워넣어져서 그 상태에서 변경하게 할 것이다.

 

그렇기 때문에 

input textarea가 빈값에서 시작하는 createNote함수는

post요청을 보내게 될 것이고 id는 데이터베이스에 저장될 때 만들어 진다.

 

api주소는 

뒤에서 api router 생성할 때 다룰 것이다!

 

deleteNote함수는 단순히 id를 파라미터로 받아 지우는 것

그 이상도 그 이하도 아니다.

 

이제 로직은 끝났고

prisma를 이용해서 

API router를 만들어보자아아아아아

 

 

pages폴더내에

api폴더를 만들어준 다음

create함수에서 사용할 파일을 만들자

 

경로는

pages/api/create.ts가 될 것이다.

 

 

create.ts

 

import { prisma } from '../../lib/prisma';
import { NextApiRequest, NextApiResponse } from 'next';

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const { title, content } = req.body.data;
  try {
    await prisma.note.create({
      data: {
        title,
        content,
      },
    });
    res.status(200).json({ message: 'Note Created' });
  } catch (error) {
    console.log(error);
  }
}

prisma와

 

handler에서 사용할 

request와 response의 타입을 next에서 import해오자.

 

클라이언트에서 form태그로

title과 content 밸류를 담아 request하는데

구조분해할당하여 가져온다.

 

다음

prisma.note.create 

메소드를 이용하여 

data에 담아 생성해준다.

 

성공할경우

response의 상태는 200이며 note가 만들어졌다는 메세지를 받을 수 있다.

 

그 후 

api폴더 안에 

note폴더를 만든 후 

[id].ts 파일을 생성하자

nextJS는 

폴더명기반으로 동적 라우팅 경로를 자동으로 적용해주기 때문에

API주소는

http://localhost:3000/api/note/[id]

가 되는 것이다.

 

대괄호를 넣어준 것은 

파라미터로 숫자를 받겠다는 뜻이다

 

import { prisma } from '../../../lib/prisma';
import { NextApiRequest, NextApiResponse } from 'next';

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method === 'DELETE') {
    const id = req.query.id;
    const note = await prisma.note.delete({
      where: {
        id: Number(id),
      },
    });
    res.json(note);
  } else if (req.method === 'PUT') {
    const { title, content, id } = req.body.data;
    const note = await prisma.note.update({
      where: {
        id: Number(id),
      },
      data: {
        id,
        title,
        content,
      },
    });
    res.json(note);
  }
}

 

이제 

http://localhost:3000/api/note/[id]

주소로 요청을 보낼 때 

메소드가 delete인지 put인지에 따라

 다른 결과를 내뱉는다.

 

delete일 경우 

prisma.note.delete로

받아온 id값을 찾아 지우고

 

put일 경우

prisma.note.update로 

받아온 id값의 데이터를 새로운 데이터값으로 덮어씌운다.

 

둘 다 res.json(note)로

해당 note를 json형식으로 바꿔서 교체해줘야한다.

 

api router는 끝났다!

 

 

 

드디어 완성됐다.

이렇게 api router까지 만들어서

투두리스트 CRUD는 끝났는데

다음 시리즈로 생각하고 있는 건

역시나 api router를 이용한 풀스택으로

jwt기반의 인증 인가 기능을 구현해보려고 한다.

 

커밍 쑨...!!!

 

 

 

투두 리스트 깃헙 주소는 

https://github.com/LuisKlopp/next-todolist

위와 같다.

 

 

 

반응형