[NextJs] - 풀 스택 투두리스트 만들기 (마지막) (feat. SSR, Generic, Api Router)
모든 준비는 끝났다!
투두리스트 지금 바로 시작~~~합니다.
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
위와 같다.