1. Streaming : 서버가 클라이언트로 데이터를 한 번에 전달하지 않고, 준비된 부분부터 점진적으로 전송하는 방식. UX와 SEO를 동시에 개선할 수 있다.
2. Suspense : React에서 비동기 작업의 상태를 관리하는 컴포넌트. 로딩 중에는 fallback 속성을 통해 대체 UI를 보여줄 수 있다.
3. Trigger : 데이터베이스에서 특정 이벤트(예: INSERT, UPDATE)가 발생했을 때 자동으로 실행되는 SQL 함수. (js 의 이벤트리스너와 유사하게 동작함)
Loading UI
https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming
Routing: Loading UI and Streaming | Next.js
Built on top of Suspense, Loading UI allows you to create a fallback for specific route segments, and automatically stream content as it becomes ready.
nextjs.org
loading.tsx 가 layout.tsx 및 그 하위 경로의 page.tsx가 렌더링되는 동안 로딩상태를 표시합니다.
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import QueryProvider from "@/components/QueryProvider";
import { Suspense } from "react";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "포켓몬 도감",
description: "이거슨 포켓몬 도감",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>
<header className="text-2xl text-center mt-4 mb-4">포켓몬 도감</header>
<QueryProvider>
{/* 바로 이 Suspense fallback 부분을 Next.js는 loading.tsx로 인식합니다. */}
<Suspense fallback={<div>loading.tsx 컴포넌트</div>}>
{children}
</Suspense>
</QueryProvider>
</body>
</html>
);
}
- <Suspense>: 비동기 작업을 처리할 때 사용할 수 있는 컴포넌트로, 로딩 상태를 관리합니다.
- fallback: 비동기 작업이 완료되기 전까지 보여줄 컴포넌트를 정의합니다.
https://react.dev/reference/react/Suspense
<Suspense> – React
The library for web and native user interfaces
react.dev
- Next.js 에러처리 패턴
- 클라이언트 컴포넌트에서의 에러처리
// useQuery, useInfiniteQuery 에 대한 에러 처리
function makeQueryClient() {
return new QueryClient({
queryCache: new QueryCache({
onError: (error, query) => {
alert(`Error Occured in ${query.queryKey}: ${error.message}`);
},
}),
defaultOptions: {
queries: {
staleTime: 60 * 1000,
retry: 0,
},
},
});
}
// useMutation 의 경우 onError 콜백 사용
import { useMutation } from "@tanstack/react-query";
function MyComponent() {
const mutation = useMutation(
{
mutationFn: async (data) => {
const res = await fetch("/api/action", {
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
});
if (!res.ok) {
const errorData = await res.json();
throw new Error(errorData.message || "서버 오류");
}
return res.json();
},
onError: (error, variables, context) => {
console.error("Mutation 실패:", error);
alert(`요청 실패: ${error.message}`);
},
}
);
return (
<button onClick={() => mutation.mutate({ key: "value" })}>
서버 액션 실행
</button>
);
}
서버 컴포넌트에서의 에러처리
전역에러처리로 global-error.tsx 을 app/ 에 만들어두면 네트워크 오류와 같은 전역적 에러 발생했을 때 에러를 받고 화면에 나타내줄 수 있습니다. 각 page 별 서버컴포넌트는 에러는 error.tsx 를 각 page.tsx 와 같은 레벨로 만들면 에러를 받아 처리가능 합니다.
// src/app/global-error.tsx
"use client";
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<html>
<body className="min-h-screen bg-gradient-to-b from-gray-50 to-gray-100 flex items-center justify-center p-4">
<div className="max-w-md w-full bg-white rounded-lg shadow-lg p-8 text-center">
<div className="mb-6">
<svg
className="w-16 h-16 text-red-500 mx-auto mb-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
<h2 className="text-2xl font-bold text-gray-800 mb-2">
문제가 발생했습니다!
</h2>
<p className="text-gray-600 mb-6">{error.message}</p>
</div>
<button
onClick={() => reset()}
className="bg-blue-500 hover:bg-blue-600 text-white font-semibold py-2 px-6 rounded-md transition duration-200 ease-in-out transform hover:scale-105 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50"
>
다시 시도하기
</button>
</div>
</body>
</html>
);
}
https://nextjs.org/docs/14/app/api-reference/file-conventions/error
File Conventions: error.js | Next.js
API reference for the error.js special file.
nextjs.org
- SSR 페이지 Streaming
- Streaming 이란?
- Streaming은 서버가 클라이언트(브라우저)에게 웹 페이지의 내용을 한 번에 전부 보내는 대신, 부분적으로 점진적으로 보내는 방식입니다. 이는 페이지가 로딩되는 동안 일부 콘텐츠가 준비되면 그 부분부터 먼저 사용자에게 보여주기 시작하는 것을 의미합니다.
page.tsx 내에서 Suspense로 Streaming 적용 시 SEO 와 UX 두마리 토끼를 잡을 수 있습니다.
// page.tsx (SSR 페이지)
export default async function SSRPage() {
await fetch("서버URL"); // 이 부분은 loading.tsx 에서 처리
return (
<>
<Suspense fallback={<div>로딩중...</div>}>
<SSRComponent1 />
</Suspense>
<Suspense fallback={<div>로딩중...</div>}>
<SSRComponent2 />
</Suspense>
</>
)
}
Streaming 을 사용한 SSR 페이지의 html response(doc)를 확인해 봅시다.
(FAQ) Streaming 해도 SEO에 문제없을까요?
https://vercel.com/guides/does-streaming-affect-seo
Does streaming affect SEO and can streamed content be indexed?
Streamed content does not affect SEO and will still be indexed by Google. Learn more in this guide.
vercel.com
- Next.js middleware
- 미들웨어(Middleware)는 특정 경로에 대한 요청이 서버에서 처리되기 전에 중간에 어떤 작업을 수행하고 싶을 때 사용합니다.
대표적인 용도로 인가 처리(Authorization) 이 있습니다.
- 알아두면 좋은 NextResponse 의 4가지 메소드 의미
// 요청을 계속 처리. 단순히 사용자 요청을 그대로 진행시킵니다.
return NextResponse.next();
// about 경로로 redirect 시킵니다.
return NextResponse.redirect(new URL('/about', request.url));
// faq 경로로 rewrite 합니다. (url path는 그대로인데 실행되는 페이지 컴포넌트가 다릅니다.)
return NextResponse.rewrite(new URL("/faq", request.url));
// 요청으로 가기전에 json 데이터를 응답합니다.
return NextResponse.json({ data: { hello: "world" } });
권한에 따른 redirect 로직
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith("/profile")) {
const token = request.cookies.get("token")?.value;
if (!token) {
// token 이 없는 상태로 프로필페이지에 접근 시도 시 홈 화면으로 리다이렉트 시킵니다.
return NextResponse.redirect(new URL("/", request.url));
}
return NextResponse.next();
}
}
export const config = {
matcher: [
"/((?!api|_next/static|_next/image|favicon.ico).*)",
],
};
권한에 따른 rewrites 로직
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
// This function can be marked `async` if using `await` inside
export function middleware(request: NextRequest) {
const membershipLevel = request.cookies.get('membershipLevel')?.value || 'guest';
if (request.nextUrl.pathname.startsWith("/posts")) {
if (membershipLevel === "admin") {
// 회원등급이 admin 인 경우 관리자 전용 페이지를 볼 수 있게 인가처리할 수도 있습니다.
return NextResponse.rewrite(new URL("/admin/posts", request.url));
}
return NextResponse.next();
}
}
export const config = {
matcher: [
"/((?!_next/static|_next/image|favicon.ico).*)",
],
};
권한없는 api 요청 시 빠른 응답
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith("/api/profile")) {
const token = request.cookies.get("token")?.value;
if (!token) {
return NextResponse.json({ message: "token 이 없습니다." });
}
}
return NextResponse.next();
}
export const config = {
matcher: [
"/((?!_next/static|_next/image|favicon.ico).*)",
],
};
middleware.ts 파일은 src/ 에 위치해야 합니다. app/ 안으로 넣으면 동작하지 않습니다.
https://nextjs.org/docs/14/app/building-your-application/routing/middleware
Routing: Middleware | Next.js
Learn how to use Middleware to run code before a request is completed.
nextjs.org
utils/supabase/middleware.ts 설정 일부 수정
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'
export async function updateSession(request: NextRequest) {
let supabaseResponse = NextResponse.next({
request,
})
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll()
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) => request.cookies.set(name, value))
supabaseResponse = NextResponse.next({
request,
})
cookiesToSet.forEach(({ name, value, options }) =>
supabaseResponse.cookies.set(name, value, options)
)
},
},
}
)
// IMPORTANT: Avoid writing any logic between createServerClient and
// supabase.auth.getUser(). A simple mistake could make it very hard to debug
// issues with users being randomly logged out.
const {
data: { user },
} = await supabase.auth.getUser()
if (
!user &&
!request.nextUrl.pathname.startsWith('/login')
) {
// no user, potentially respond by redirecting the user to the login page
const url = request.nextUrl.clone()
url.pathname = '/login'
return NextResponse.redirect(url)
}
// 이 부분 수정함. 현재 로그인 상태이면서 경로가 /login 인 경우 홈화면으로 리다이렉트.
if (user && request.nextUrl.pathname.startsWith("/login")) {
return NextResponse.redirect(request.nextUrl.origin);
}
// IMPORTANT: You *must* return the supabaseResponse object as it is. If you're
// creating a new response object with NextResponse.next() make sure to:
// 1. Pass the request in it, like so:
// const myNewResponse = NextResponse.next({ request })
// 2. Copy over the cookies, like so:
// myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())
// 3. Change the myNewResponse object to fit your needs, but avoid changing
// the cookies!
// 4. Finally:
// return myNewResponse
// If this is not done, you may be causing the browser and server to go out
// of sync and terminate the user's session prematurely!
return supabaseResponse
}
- supabase general client 설정
- 서버 컴포넌트, 클라이언트 컴포넌트 구분없이 어디서든 편안하게 사용할 수 있는 client 설정
const supabase = typeof window === undefined ? createServerClient() : createBrowserClient();
route handler 에서의 supabase client 는 반드시 매번 새롭게 정의 해줘야 합니다.
route handler 는 서버리스 환경에서 동작하여 각 요청이 독립적인 상태를 가지는 특징을 가지고 있습니다.
import { createClient } from "@/utils/supabase/server";
export async function POST(request: Request) {
const supabase = createClient();
...
}
- Supabase SignUp Trigger (회원가입 시 자동으로 users 테이블에 회원등록 연동)
- 회원가입 요청 api
let { data, error } = await supabase.auth.signUp({
email: "본인이메일",
password: "본인비밀번호",
options: {
data: {
nickname: "본인닉네임",
},
},
});
- Trigger 생성 및 DB 함수 생성 SQL 명령어 템플릿
- 아래 명령어를 SQL Editor 에 붙여넣고 실행하세요.
-- 새로운 트리거 함수 생성
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO public.users (id, email, nickname)
VALUES (NEW.id, NEW.email, NEW.raw_user_meta_data->>'nickname');
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- 새로운 트리거 생성
CREATE TRIGGER on_auth_user_created
AFTER INSERT ON auth.users
FOR EACH ROW
EXECUTE FUNCTION public.handle_new_user();
- 트리거 삭제 SQL 명령어
- 실수로 트리거를 잘못 만들었을 때는 삭제하고 다시 만들어야 합니다
DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users;
handle_new_user 라는 DB Function은 CREATE OR REPLACE FUNCTION 로 생성되었기 때문에 뭔가 실수가 있었더라도 삭제하지 않고 수정 후 다시 실행하면 됩니다.
- Supabase with Typescript
- Supabase api 에 관한 타입생성을 간단하게 할 수 있는 설정을 제공하고 있습니다.
https://supabase.com/docs/guides/api/rest/generating-types
Generating TypeScript Types | Supabase Docs
How to generate types for your API and Supabase libraries.
supabase.com
package.json 의 scripts 에 타입생성 명령 셋업
# package.json
"scripts": {
"genTypes": "supabase gen types typescript --project-id $PROJECT_REF --schema public > src/types/supabase.ts"
},
- useState 상태 타입 지정
- 만약 todos 테이블에서 관리하는 todo 데이터 타입을 가져오고 싶을 때
import { Database, Tables, Enums } from "./database.types.ts";
// Before 😕
let todo: Database['public']['Tables']['todos']['Row'] = // ...
// After 😍
let todo: Tables<'todos'>
// useState 예시
const [todo, setTodo] = useState<Tables<'todos'> | null>(null);
- Supabase Login by Server Client
- Server Client로 로그인 성공 시 쿠키에 세션 정보가 저장됩니다. (Browser Client로 로그인 시 localStorage에 저장되었었죠?)
'부캠 > TypeScript&Next' 카테고리의 다른 글
[NEXT]- 마지막 팀 과제에 쓴 거 정리 (1) (0) | 2024.12.30 |
---|---|
[NEXT]STANDARD-플러스주차 복습 #7 (0) | 2024.12.26 |
[NEXT] - STANDARD플러스주차 복습 #6 (0) | 2024.12.23 |
[NEXT]Basic zoom (2) middleware & supabase ssr (0) | 2024.12.21 |
[NEXT]Basic zoom (1) route handler (0) | 2024.12.21 |