제어 컴포넌트
: 리액트에서 값이 제어되는 컴포넌트
비제어 컴포넌트
: 리엑트에서 값이 제어되지 않는 컴포넌트
yarn add react-hook-form
"use client";
import { useForm } from "react-hook-form";
const SignInForm = () => {
const {
register,
handleSubmit,
formState: { errors },
} = useForm();
const onSubmit = (data) => {
console.log(data);
};
return (
<form
onSubmit={handleSubmit(onSubmit)}
className="flex flex-col gap-4 p-5 items-center w-full m-auto"
>
<div className="flex flex-col gap-2">
<label htmlFor="email">Email</label>
<input
id="email"
type="text"
{...register("email", {
required: "Email is required",
pattern: {
value: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
message: "Invalid email address",
},
})}
placeholder="Email"
className="text-black"
/>
{errors.email && (
<p className="text-red-500">{errors.email.message}</p>
)}
</div>
<div className="flex flex-col gap-2">
<label htmlFor="password">Password</label>
<input
type="password"
{...register("password", {
required: "Password is required",
})}
placeholder="Password"
className="text-black"
/>
{errors.password && (
<p className="text-red-500">{errors.password.message}</p>
)}
</div>
<button
className="bg-gray-800 text-white px-4 py-2 rounded-md"
type="submit"
>
Sign In
</button>
</form>
);
};
export default SignInForm;
Zod는 TypeScript와 JavaScript를 위한 스키마 선언 및 검증 라이브러리
yarn add zod
import { z } from 'zod';
const userSchema = z.object({
firstName: z.string().min(1, "First name is required"),
lastName: z.string().min(1, "Last name is required"),
});
try {
userSchema.parse({ firstName: "John", lastName: "" });
} catch (e) {
console.error(e.errors);
}
yarn add @hookform/resolvers
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { FieldValues, useForm } from "react-hook-form";
import { z } from "zod";
const signInSchema = z.object({
email: z.string().email({ message: "invalid email" }).min(1, {
message: "email required",
}),
password: z.string(),
});
const SignInForm = () => {
const { register, handleSubmit, formState } = useForm({
mode: "onChange",
defaultValues: {
email: "",
password: "",
},
resolver: zodResolver(signInSchema),
});
const onSubmit = async (value: FieldValues) => {
const res = await fetch("/api/sign-in", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
...value,
}),
});
const data = await res.json();
console.log(data);
};
return (
<form
onSubmit={handleSubmit(onSubmit)}
className="flex flex-col gap-4 p-5 items-center w-full m-auto"
>
<div className="flex flex-col gap-2">
<label htmlFor="email">Email</label>
<input
{...register("email")}
placeholder="Email"
className="text-black"
/>
{formState.errors.email && (
<span>{formState.errors.email.message}</span>
)}
</div>
<div className="flex flex-col gap-2">
<label htmlFor="password">Password</label>
<input
type="password"
{...register("password")}
placeholder="Password"
className="text-black"
/>
</div>
<button
disabled={!formState.isValid}
className="bg-gray-800 text-white px-4 py-2 rounded-md"
type="submit"
>
Sign In
</button>
</form>
);
};
export default SignInForm;
"use client";
import React from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const schema = z.object({
email: z.string().email("Invalid email").nonempty("Email is required"),
password: z.string().nonempty("Password is required"),
});
const SignInForm = () => {
const {
register,
handleSubmit,
formState: { errors },
} = useForm({
resolver: zodResolver(schema),
mode: 'onBlur',
asyncResolver: async (values) => {
const result = await fetch('/validate-email', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ email: values.email }),
});
const data = await result.json();
if (!data.isValid) {
return {
values: {},
errors: {
email: { message: "Email already taken" },
},
};
}
return { values, errors: {} };
},
});
const onSubmit = (data) => {
console.log(data);
};
return (
<form
onSubmit={handleSubmit(onSubmit)}
className="flex flex-col gap-4 p-5 items-center w-full m-auto"
>
<div className="flex flex-col gap-2">
<label htmlFor="email">Email</label>
<input
id="email"
type="text"
{...register("email")}
placeholder="Email"
className="text-black"
/>
{errors.email && (
<p className="text-red-500">{errors.email.message}</p>
)}
</div>
<div className="flex flex-col gap-2">
<label htmlFor="password">Password</label>
<input
type="password"
{...register("password")}
placeholder="Password"
className="text-black"
/>
{errors.password && (
<p className="text-red-500">{errors.password.message}</p>
)}
</div>
<button
className="bg-gray-800 text-white px-4 py-2 rounded-md"
type="submit"
>
Sign In
</button>
</form>
);
};
export default SignInForm;
(app디렉토리와 동일한위치에 middleware.ts생성)
middleware.ts
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) {
return NextResponse.redirect(new URL('/home', request.url));
}
export const config = {
matcher: '/about/:path*',
};
export const config = {
matcher: '/about/:path*',
};
export const config = {
matcher: ['/about/:path*', '/dashboard/:path*'],
};
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
};
만약 동적인 값으로 matcher를 작동시키고 싶거나 path가 동일하더라도 특정 조건에만 middleware를 작동시켜야 한다면 middleware 함수 내부에 조건문에서 이를 충족시킬 수 있음
예제 코드에서는 middleware에서 response를 생성해서 사용자에게 바로 응답을 전달
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith('/about')) {
return NextResponse.rewrite(new URL('/about-2', request.url));
}
if (request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.rewrite(new URL('/dashboard/user', request.url));
}
}
NextResposne를 통해 사용자를 특정 url로 리다이렉트 시키거나 요청한 url은 그대로 노출하면서 특정 경로로 이동시키는 rewrite를 수행
yarn add -D json-server-auth
json-server-auth middleware를추가yarn auth
쿠키 관리 기능
- 쿠키 읽기: 특정 쿠키를 가져오거나 모든 쿠키를 가져올 수 있다.
- 쿠키 존재 여부 확인: 쿠키의 존재 여부를 확인할 수 있다.
- 쿠키 설정: 새로운 쿠키를 설정하거나 기존 쿠키를 업데이트할 수 있다.
- 쿠키 삭제: 쿠키를 삭제할 수 있는 여러 가지 방법을 제공한다.
import { cookies } from 'next/headers'
export default function Page() {
const cookieStore = cookies()
const theme = cookieStore.get('theme')
return '...'
}
cookies().get(name) 메서드를 사용하여 특정 이름의 쿠키를 가져올 수 있습니다. 만약 쿠키가 없으면 undefined를 반환import { cookies } from 'next/headers'
export default function Page() {
const cookieStore = cookies()
return cookieStore.getAll().map((cookie) => (
<div key={cookie.name}>
<p>Name: {cookie.name}</p>
<p>Value: {cookie.value}</p>
</div>
))
}
import { cookies } from 'next/headers'
export default function Page() {
const cookieStore = cookies()
const hasCookie = cookieStore.has('theme')
return '...'
}
cookies().has(name) 메서드를 사용하여 특정 쿠키가 존재하는지 여부를 확인'use server'
import { cookies } from 'next/headers'
async function create(data) {
cookies().set('name', 'lee')
// or
cookies().set('name', 'lee', { secure: true })
// or
cookies().set({
name: 'name',
value: 'lee',
httpOnly: true,
path: '/',
})
}
'use server'
import { cookies } from 'next/headers'
async function delete(data) {
cookies().delete('name')
}
2. 빈 값 설정
'use server'
import { cookies } from 'next/headers'
async function delete(data) {
cookies().set('name', '')
}
3.만료 시간 설정
'use server'
import { cookies } from 'next/headers'
async function delete(data) {
cookies().set('name', 'value', { maxAge: 0 })
}
yarn add @tanstack/react-query
// In Next.js, this file would be called: app/providers.jsx
"use client";
// Since QueryClientProvider relies on useContext under the hood, we have to put 'use client' on top
import {
isServer,
QueryClient,
QueryClientProvider,
} from "@tanstack/react-query";
function makeQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
// With SSR, we usually want to set some default staleTime
// above 0 to avoid refetching immediately on the client
staleTime: 60 * 1000,
},
},
});
}
let browserQueryClient: QueryClient | undefined = undefined;
function getQueryClient() {
if (isServer) {
// Server: always make a new query client
return makeQueryClient();
} else {
// Browser: make a new query client if we don't already have one
// This is very important, so we don't re-make a new client if React
// suspends during the initial render. This may not be needed if we
// have a suspense boundary BELOW the creation of the query client
if (!browserQueryClient) browserQueryClient = makeQueryClient();
return browserQueryClient;
}
}
export default function Providers({ children }: { children: React.ReactNode }) {
// NOTE: Avoid useState when initializing the query client if you don't
// have a suspense boundary between this and the code that may
// suspend because React will throw away the client on the initial
// render if it suspends and there is no boundary
const queryClient = getQueryClient();
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
}
src>app>layout.tsx
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import Providers from "@/components/providers/RQProvider";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>
<Providers>{children}</Providers>
</body>
</html>
);
}
: 간소화된 Flux원리를 기반으로 한 상태관리 라이브러리
yarn add zustand
import { Product } from "@/type/product";
import { createStore } from "zustand";
export type CartProduct = Product & { quantity: number };
export interface CartProps {
products: CartProduct[];
}
export interface CartState extends CartProps {
addProduct: (product: CartProduct) => void;
}
export type CartStore = ReturnType<typeof createCartStore>;
export const createCartStore = (initProps?: Partial<CartProps>) => {
const DEFAULT_PROPS: CartProps = {
products: [],
};
return createStore<CartState>()((set) => ({
...DEFAULT_PROPS,
...initProps,
addProduct: (product: CartProduct) =>
set((state) => ({
products: [...state.products, product],
})),
}));
};
"use client";
import { useContext, useRef } from "react";
import { CartProps, CartState, CartStore, createCartStore } from "@/cartStore";
import { createContext } from "react";
import { useStore } from "zustand";
export function useCartContext<T>(selector: (state: CartState) => T): T {
const store = useContext(CartContext);
if (!store) throw new Error("Missing CartContext.Provider in the tree");
return useStore(store, selector);
}
export const CartContext = createContext<CartStore | null>(null);
type CartProviderProps = React.PropsWithChildren<CartProps>;
export function CartProvider({ children, ...props }: CartProviderProps) {
const storeRef = useRef<CartStore>();
if (!storeRef.current) {
storeRef.current = createCartStore(props);
}
return (
<CartContext.Provider value={storeRef.current}>
{children}
</CartContext.Provider>
);
}
npx create-next-app@latest my-app --typescript --tailwind --eslint
shadcn/ui설치
npx shadcn-ui@latest init