콤퓨우터/프로그래밍

Next.js Full Course 필기 (3)

파란화면 2024. 3. 17. 16:49
반응형

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 함수:
      1. e.preventDefault() 를 통해 페이지가 새로고침되는것을 방지한다.
      1. 브라우저 Form상의 데이터를 가져온다.
      1. 백엔드 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
    HTTP PUT에 해당되는 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);
}

 

반응형