FACTS(사실, 객관) : 내가 한 일
next.js
인증인가 서버 만들기
yarn add -D json-server-auth
package.json 파일 script구간에 추가
"scripts" : {
"auth" : "json-server-auth --watch auth-db.json --port 8000"
}
루트디렉토리에 auth-db.json 생성하고 일단 가상의 유저를 넣어줌
{
"users" : [
{
"id" : 1,
"email" : "user@example.com",
"password" : "password"
}
]
}
터미널에 yarn auth를 적어 실행시킴.
yarn auth
여기서 새 유저 만들기, 로그인, 간단한 인가플로우를 제공해줌.
json-server-auth에서는 auth-db.json에 있는 비밀번호를 읽기 어렵게 변경해 놓음. => 보안에 아주? 굿인 라이브러리
api라우트 sign-in을 만들어줌
app > api > sign-in > rout.ts
import { cookies } from "next/headers";
import { NextResponse } from "next/server";
export async function POST(request:Request) {
const payload = await request.json();
const res = await fetch("http://localhost:8000/signIn", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
cache: "no-store", // 라우트핸들러 fetch cache가 생기지 않도록 no-store
body: JSON.stringify({
...payload,
})
})
const data = await res.json();
const cookieStore = cookies();
cookieStore.set("accessToken", data.accessToken) ,{
httpOnly: true,
}) // 이렇게 하면 브라우저 자바스크립트로는 이 cookie에 접근할 수 없게 되고 서버환경에서만 접근할 수 있게 됨.
return NextResponse.json({
data: true,
})
} // data: true를 반환해주는 간단한 라우트!
그리고 SignInForm.tsx에
const onSubmit = async (value: FieldValues) => {
const res = await fetch("/api/sign-in",{
method: "POST",
header: {
"Content-Type" : "appliction/json"
},
body: JOSN.stringify({
...value,
}),
});
}
그리고 미들웨어에서
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) {
console.log(request.cookies) // accessToken의 value값 ("accessToken" : {name: 이름, value: "~~"})
console.log(request.cookies.get("accessToken")) // value값 ({name: 이름, value: "~~"})
console.log(request.cookies.get("accessToken")?.value) // accessToken값만 (~~)
const isLogin = !!request.cookies.get("accessToken")?.value; // isLogin이라는 값 안에 토큰이 있다면??
if(!isLogin && request.nextUrl.pathname.includes('/cart')){
return NextResponse.redirect(new URL('/sign-in', request.url));
}
return NextResponse.redirect(new URL('/home', request.url));
}
export const config = {
matcher: '/about/:path*',
};
이렇게 서버에서 쿠키를 넣어준 로직들이나 header에 내가 넣어준 로직들, 브라우저에 담겨있는 것들을 미들웨어, 라우트핸들러에 불러와서 분기해줄 수 있음.!
이렇게 쿠키를 이용할 수 있는것 => next.js의 이점!
이걸 이용해 백엔드의 인증인가 서버가 없거나 이런 정책이 잘 정해져있지 않아도 프론트엔드에서 사용자들이 더 안전하게 페이지에 접근할 수 있게 UX를 올려주는 프로그래밍도 쉽?게? 가능해짐!!
미들웨어에서 (쿠키)값을 가져와야만 하는것은 아님!!
레이아웃 안에서도 cookies()를 통해서 쿠키값을 가져올 수 있음! (서버컴포넌트 안이라면 얼마든지!)
근데? cookies를 쓰면 다이나믹렌더링 페이지가 되어버림~
=> 미들웨어 등, 혹은 API라우트 안에서 쿠키에 접근하는 방법을 권장
리액트쿼리
서버상태 관리 라이브러리~ 리액트나 next.js에서도 많이쓰임!
비동기 네트워크 통신 안에서 여러가지 서버상태들(캐싱, 에러, 로딩, refecth)을 간편하게 옵션을 이용해 만들 수 있음!
장점?
가장큰거 => 데이터 fetching을 단순화 할 수 있음!
특징
1. query key 기반의 캐싱 => 반복된 네트워크 요청시에 불필요한 요청이 줄어듦 여러 컴포넌트에서 동일한 데이터가 사용될 때 같은 데이터 네트워크 fetching을 공유할 수 있게 됨.
const { data } = useQuery({
queryKey: ['dataKey'],
queryFn: getServerData
});
// 이후 동일한 queryKey를 사용하면 캐싱된 데이터를 사용합니다.
const { data: cachedData } = useQuery({
queryKey: ['dataKey'],
queryFn: getServerData
});
2. Stale Time / Cache Time 두개의 시간을 가지고 캐싱함 => 유효한 데이터를 판단하는시간(Stale time), fallback 데이터를 판단하는 시간(gcTime -> (Cache Time))
3. 클라이언트 데이터와 서버 데이터간의 분리
useMutation으로 서버 데이터를 변경할 때에도 onSuccess와 onError함수를 통해 fetch의 성공실패 분기를 간단하게 분리할 수 있고, 내가 사용하고 있는 쿼리 데이터를 변경하는 등의 행동들도 자유롭게 할 수 있어서 클라이언트 데이터와 서버데이터간의 상태를 완전히 격리해 React Query 내부에서 데이터를 관리할 수 있게 해준다는 장점이 있다
ReactQuery 대표 기능
1. useQuery
GET 요청(가져오기) 에서만 사용
2. useMutation
PUT, UTDATE, DELETE 등 데이터를 변형할 때 사용
yarn add @tanstack/react-query-devtools
src > components > providers > RQProvider.tsx
// In Next.js, this file would be called: app/providers.jsx
"use client"; // provider에서는 항상 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}>
<ReactQueryDevtools />
{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<{
childern: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>
<Providers>{children}</Providers>
</body>
</html>
)
}
여기서 Provider를 만들 때 use client로 만듦
근데 use client 하위에 있는 컴포넌트들은 클라이언트로 동작하게 됨.
그럼 use client를 사용해서 내려주면 결국 page.tsx는 Provider라는 컴포넌트 안에 있는 거고 그러면 서버에서 로직을 실행시키는게 아니지 않나?? 이게 서버로직이 아닌거 아닌가?? 라는 생각이 들 수 있음!!!
=> 제일 이해하기 어려운 부분 !
=> "use client"는 100% 클라이언트 사이드 렌더링이 아님
클라이언트 컴포넌트는 무조건 서버에서 프리 렌더링 과정 즉 사전 렌더링 과정을 거침.
아무리 use client로 감싸더라도 어느정도 정적 콘텐츠가 같이 내려온다는 뜻. 그런 다음에 use client로 작성된 곳 안에서 리액트 hook, onClick 이벤트 같은 클라이언트 메서드를 써야 하는 것들만 JS번들로 자동으로 분리돼서 내려준다는 것.
=> 그럼 children은 어떻게 된건가??
use client를 쓰고 children props로 클라이언트 안에서 서버컴포넌트 트리를 children으로 받게 되면 그 곳만 클라이언트 컴포넌트 로직으로 동작하고, children에 들어가 있는 것은 다시 이 컴포넌트 안에서 use client를 선언해주기 전까지는 서버컴포넌트로 동작하게 됨.
이렇게 서버 컴포넌트 트리와 클라이언트 컴포넌트 트리를 중첩해서 사용할 수 있고, 이런것들을 next.js에서 컴포지션 패턴이라고 함.
(https://github.com/reactwg/server-components/discussions/4)
next.js의 client components docs에도 클라이언트 컴포넌트는 인터랙티브 UI를 할 수 있도록 허용해 준다. 그러나 prerendered on server, 서버에서 사전 렌더링이 된다고 말하고 있음. 즉, 클라이언트 사이드 렌더링이 일부 동작하는 것이 100% csr이 아니다 라는 뜻.
레이아웃에서 use client를 사용하더라도 서버컴포넌트들을 children으로 잘 내려주고만 있었다면 괜찮다! 안에서도 서버 컴포넌트를 쓸 수 있다 ! 라는 뜻.
FEELINGS(느낌, 주관) : 나의 감정적인 반응, 느낌
FINDINGS(배운 것) : 그 상황으로부터 내가 배운 것, 얻은 것
FUTURE(미래) : 배운 것을 미래에는 어떻게 적용할 지