Fireship.io의 "Next.js Full Course" 강의를 듣고 필기했던 내용입니다.
Protecting Routes from User
예를 들어, 로그인하지 않은 사용자에게는 특정 라우트에 접근하지 못하게 하고 싶을 수 있다
서버 컴포넌트에서 그렇게 하지 못 하게 하는 가장 좋은 방법은, getSeverSession()
을 쓰는 것이다 (로그인이 되어 있지 않다면 NULL
이 될 것이기 때문)
따라서
const session = await getServerSession();
if(!session) {
// Option #1 - 'next/navigation'의 redirect 사용하여 로그인 페이지로 보내버리기
redirect('/api/auth/signin');
// Option #2
return <p>You must sign in to see this content</p>;
}
Prisma
ORM (Object Relation Mapper)이다
설정을 마쳤다면 /lib/prisma.ts
파일을 생성하여
import { PrismaClient } from "@prisma/client";
export const prisma = new PrismaClient();
Auth Datastore
지금까지는 유저 데이터를 저장을 안 했다
이제는 >>>데이터베이스<<< 가 있기때문에 여기에 세션을 저장하고자 하였다
npm install @prisma/client @next-auth/prisma-adapter
이후 /app/api/auth/[...nextauth]/route.ts
의 NextAuthOptions 부분에
export const authOptions: NextAuthOptions = {
adapter: PrismaAdapter(prisma),
providers:
(...)
}
를 추가
하지만!!!
The CredentialsProvider is not compatible with Database Sessions. In order for credentials to work, you need to configure Next-Auth to use JWT sessions instead of Database sessions.
즉, next-auth에서 비밀번호 로그인 등을 구현하기 위해 Credentials Provider를 이용하였다면 jwt만 쓸 수 있다. 주의!!!!!!
DB의 데이터를 접근하는 API Route 만들어보기
/app/api/users/route.ts
import { prisma } from '@/lib/prisma'
import { NextResponse } from 'next/server'
export async function GET(req: Request) {
const users = await prisma.user.findMany();
return NextResponse.json(users);
}
- Prisma ORM에 의해
prisma.user
를 통해 Users 테이블의 데이터를 객체지향-적으로 접근할 수 있음 - NextResponse.json()을 통해 JSON 형식으로 반환
Server Component에서 DB 데이터 받아오기
Next.js에서는, 굳이 API Route에 fetch()를 해서 데이터베이스의 데이터를 받아오지 않아도, 그냥 Server Component에서 Prisma를 통해 DB에 직접 접속할 수 있다.
/app/users/page.tsx
:
import { prisma } from "@/lib/prisma";
import styles from './page.module.css';
import UserCard from '@/app/components/UserCard';
export default async function Users() {
const users = await prisma.user.findMany();
return (
<div className={styles.grid}>
{users.map((user) => {
return <UserCard key={user.id} {...user} />;
})}
</div>
);
}
주: UserCard 컴포넌트 (/app/components/UserCard.tsx
)
import Link from 'next/link';
import styles from './UserCard.module.css';
interface Props {
id: string;
name: string | null;
age: number | null;
image: string | null;
}
export default function UserCard({id, name, age, image}: Props) {
return(
<div className={styles.card}>
<div className={styles.cardContent}>
<h3>
<Link href={`/users/${id}`}>{name}</Link>
<p>Age: {age}</p>
</h3>
</div>
</div>
);
}
Dynamic Route에서의 Data fetching
서버 컴포넌트 /app/users/[id]/page.tsx
를 만든다. 이것은 url.com/users/1235
같은 식으로 매칭될것이다.
또 Props
인터페이스를 만들어 { params } 의 타입으로 쓴다. 이 params는 URL 상의[id]
를 담는다.
이 페이지는 Dynamic 하다 - 사용자가 언제 프로필을 바꿀지 예상할 수는 없는 노릇인 것이다.
또, generateMetadata() 함수를 선언하여 HTML <head>
부분의 <meta>
태그나 <title>
또한 동적으로 생성한다.
import { prisma } from '@/lib/prisma';
import { Metadata } from 'next';
interface Props {
params: {
id: string
};
}
export default async function UserProfile({params}: Props) {
const user = await prisma.user.findUnique({
where: {
id: params.id
}
});
const {name, bio, image} = user ?? {};
return (
<div>
<h1>{name}</h1>
<h3>{bio}</h3>
</div>
);
}
export async function generateMetadata({params}: Props) : Promise<Metadata> {
const user = await prisma.user.findUnique( {where: {id: params.id}} );
return {title: `User profile page of {${user?.name}}`};
}
로딩 상태 UI
Next.js 13에서는 매우 쉽게 로딩UI 처리를 구현할 수 있다. loading.tsx를 만들고 로딩 UI로 보여주고 싶은 컴포넌트를 export해주면 된다.
/app/users/loading.tsx
:
export async default function LoadingUsers() {
return <div>Loading user data</div>;
}
기본적으로 이 로딩 화면 자식 컴포넌트에도 상속된다. 이게 싫다면 자식 컴포넌트의 경로에 loading.tsx
를 새로 만들어주면 된다.
에러 UI
에러 UI는 클라이언트 컴포넌트로 선언되어야 한다. 또 export default function Error()
를 export한다.
이 컴포넌트는 error와 reset이라는 두가지 prop을 받는다. error는 에러 객체이고, reset는 페이지 컴포넌트를 재렌더하기 위한 Next.js의 special function이다.
만약 에러를 console.log()
으로 찍으려면, 그냥 React의 useEffect 훅* 을 이용해주면 error 객체가 변경될 때마다 콘솔에 로그를 찍을수있다.
또 엔드 유저에게 노출된 JSX에는 reset() 함수를 호출하는 버튼을 넣어줄 수 있다.
"use client";
import { useEffect } from "react";
export default function Error({error, reset}: {error: Error, reset: () => void}) {
useEffect(() => {
console.log(error);
}, [error]);
return (
<div>
<h2>Something went wrong</h2>
<button onClick={() => reset()}>try again</button>
</div>
);
}
리액트의 useEffect() 훅 되짚고 넘어가기
1번째 arg는 function (구동할 함수) 이고, 2번째 arg는 언제 해당 함수를 구동할지에 대한 정보를 담은 Array임.
=> 2번째 Arg인 array는 Data Dependencies를 담고 있음 (dependencies: 데이터 변경 시 함수 구동). 예를 들어,
const [count] = useState(0);
useEffect(() => {console.log("asdf")}, [count]);
을 하면 함수가 count가 바뀔 때마다 돌아감
- Array가 비어 있으면 컴포넌트가 initalise될 때에 함수가 구동됨 (= 해당 컴포넌트가 mount될 때에만 구동됨)
- Array에 Dependency를 추가하면 해당 데이터가 갱신될 때마다 함수가 구동됨 (= 매 Update 때마다 구동됨)
- useEffect() 내부의 함수에서 다른 함수를 Return하면, 컴포넌트가 Destroy될 때 Return된 함수가 구동됨
프로필 수정기능 실장
2가지의 컴포넌트를 이용하여 자기 프로필을 수정하는페이지를 만들것이다
/app/dashboard/ProfileForm.tsx
- 클라이언트컴포넌트, Validation & Form Submission/app/dashboard/page.tsx
page.tsx
:
먼저 getServerSession()을 이용하여 로그인되지 않은 사용자가 접근하면 로그인 페이지로 보내버리자
const session = await getServerSession(authOptions);
if(!session) redirect('/api/auth/signin');
그 다음은 로그인된 유저의 현재 이메일을 가져와 해당 이메일과 일치하는 사용자 정보를 가져온다
const user = await prisma.user.findUnique({
where: {
email: session?.user?.email!
}
});
그 후 클라이언트 렌더링될 폼에 정보를 넘겨준다
return (<>
<h1>Dashboard</h1>
<ProfileForm user={user} />
</>);
ProfileForm.tsx
:
- form의 onSubmit을 통해 폼의 전송을 가로채고 updateUser 함수를 부른다.
return(
<div>
<h2>Edit your profile</h2>
<form onSubmit={updateUser}>
<label htmlFor="name">Name</label>
<input type="text" name="name" defaultValue={user?.name ?? ''} />
...
- updateUser 함수:
-
e.preventDefault()
를 통해 페이지가 새로고침되는것을 방지한다.
- 브라우저 Form상의 데이터를 가져온다.
- 백엔드 API로 데이터를 쏜다.
const updateUser = async(e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); const formData = new FormData(e.currentTarget); const body = { name: formData.get('name'), ... }; const res = await fetch('/api/user', { method: 'PUT', body: JSON.stringify(body), headers: {'Content-Type': 'application/json'} }); });
-
프로필 API 구현
- API 엔드포인트:
/api/user/route.ts
HTTPPUT
에 해당되는 async function을 export한다. - 현재 세션의 이메일 주소를 쿼리한다. (클라이언트에서 요청을 변조하여 다른 사용자의 정보를 수정하는 것을 막는다.)
- 또한, JSON에서 String이 되어버린 숫자를 다시 TS의 Number형으로 바꿔줘야한다.
- 그냥 Prisma Update에 데이터를 그대로 때려박았다. 이렇게 하더라도 Prisma가 SQLi같은 것은 막아줄 테지만, 그래도 현실에서는 데이터 포맷에 어긋나지 않는지 검증하여야 할 필요가 있을것이다. 하지만 귀찮으니 여기서는 그냥 한다.
- 그 후 Prisma update()의 리턴값을 던진다.
export async function PUT(req: Request) {
const session = await getServerSession(authOptions);
const currentUserEmail = session?.user?.email!;
const data = await req.json();
data.age = Number(data.age);
const user = await prisma.user.update({
where: {
email: currentUserEmail,
},
data
});
const {password:string, ...userWoPwd} = user;
return NextResponse.json(userWoPwd);
}
'콤퓨우터 > 프로그래밍' 카테고리의 다른 글
Next.js Full Course 필기 (4) (0) | 2024.03.17 |
---|---|
Next.js Full Course 필기 (2) (0) | 2024.03.17 |
Next.js Full Course 필기 (1) (0) | 2024.03.17 |
아희와 Hello, Wolrd! - 1문자씩 분석해보았다 (2) | 2017.06.30 |