일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | |
7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 | 30 | 31 |
- 함수형프로그래밍
- ㅡ
- BlockFormattingContext
- 부모패딩
- react
- vite
- 문제해결
- tailwindCSS
- 화살표2개
- QueryClient
- parent padding
- ignore padding
- useQueryClient
- BFC
- twoarrow
- es6
- accordian
- 제어컴포넌트
- ?? #null병합연산자
- transition
- 리액트
- alias설정
- debouncing
- DOM
- CustomHook
- 조건부스타일
- 서초구보건소 #무료CPR교육
- createPortal
- 부모요소의 패딩 무시
- Carousel
- Today
- Total
프론트엔드 첫걸음
nextjs learn 정리 본문
글꼴 및 이미지 최적화
글꼴
Next.js는 빌드 시 글꼴 파일을 다운로드하고 이를 다른 정적 자산과 함께 호스팅
-> 글꼴에 대한 추가 네트워크 요청이 없음 -> 성능향상웹폰트 사용은 Layout shift 생길 수 있음
fonts.ts
//next/font/google module 에서 폰트 가져옴 import { Inter, Lusitana } from 'next/font/google'; // 로드할 Inter의 하위집합 latin export const inter = Inter({ subsets: ['latin'] }); // 서브 폰트 export const lusitana = Lusitana({ weight: ['400', '700'], subsets: ['latin'], });
메인폰트사용 - layout.tsx
<body className={`${inter.className} antialiased`}>{children}</body>
서브폰트사용 - page.tsx
<p className={`${lusitana.className} text-xl text-gray-800 md:text-3xl md:leading-normal`}>
이미지
Image 컴포넌트의 이미지 최적화
- 이미지가 로드될 때 레이아웃이 자동으로 이동하는 것을 방지합니다.
- 뷰포트가 작은 기기에 큰 이미지가 전송되지 않도록 이미지 크기 조정.
- 기본적으로 이미지 지연 로딩(이미지가 뷰포트에 들어갈 때 이미지가 로드됨).
- 브라우저에서 WebP 및 AVIF와 같은 최신 형식을 지원하는 경우 이미지를 제공합니다.
Layout shift막기위해, width, height 사용할때는 소스이미지와 가로세로 비율이 동일해야함
페이지 간 탐색
- Link로 페이지 이동시 페이지 전체 새로고침이 없다.
- 자동 코드 분할 및 프리페칭
- 탐색 환경을 개선하기 위해 Next.js는 경로 세그먼트별로 애플리케이션을 자동으로 코드 분할 합니다.
- 이는 브라우저가 초기 로드 시 모든 애플리케이션 코드를 로드하는 기존 React SPA와는 다릅니다.
- 경로별로 코드를 분할 = 페이지가 격리. 특정 페이지에서 오류가 발생해도 나머지 애플리케이션은 계속 작동
- 또한 프로덕션 환경에서 컴포넌트가 브라우저의 뷰포트에 표시될 때마다 Next.js는 백그라운드에서 링크된 경로에 대한 코드를 자동으로 prefetch합니다.
- 사용자가 링크를 클릭할 때쯤이면 목적지 페이지의 코드가 이미 백그라운드에서 로드되어 있으므로 페이지 전환이 거의 즉각적으로 이루어집니다!
DB 가져오기
data fetching
- api 계층
- 클라이언트에서 데이터 가져오는 경우 Db정보가 노출되지 않도록 api계층이 필요하다
- next.js는 라우터 핸들러 사용하여 API 엔드포인트 만들 수 있다.
- DB query
- api 엔드포인트 만들때
- 서버컴포넌트에서 서버에서 데이터 불러올때 api계층 건너뛰고 DB직접 쿼리할 수 있다.
request waterfall
- 이전 요청의 완료 여부에 따라 달라지는 일련의 네트워크 요청을 의미합니다.
- 데이터 가져오기의 경우, 각 요청은 이전 요청이 데이터를 반환한 후에만 시작할 수 있습니다.
request waterfall 방지
- 모든 데이터 요청 동시에 병렬로 진행
- Promise.all() 또는 Promise.allSettled() 사용
- 하지만 하나의 데이터가 유독 느린경우에는 문제가 됨.
정적 및 동적 렌더링
- 정적렌더링
- 빌드시 렌더링 , 결과를 cdn에 배포하고 캐시할수있음
- 장점
- 더 빠른 웹사이트(미리 렌더링된 콘텐츠 캐시하여 전세계 배포~ 전세계에서 빠르고 안정적으로 접근가능)
- 서버부하 감소(콘텐츠 캐시~ 사용자요청에 따라 동적으로 콘텐츠 생성할 필요x)
- SEO 향상
- 정적 블로그 게시물이나 제품 페이지와 같이 사용자 간에 공유되는 데이터나 데이터가 없는 UI에 유용합니다.
- 동적렌더링
- request시 렌더링 .
- 장점
- 실시간 데이터 -
- 사용자별 콘텐츠
- Request time information - 요청시점에만 알수있는 정보 제공가능
스트리밍
하나의 데이터가 유독 느릴때 전체 페이지 렌더가 늦어지는 상황이 발생함 => 스트리밍 사용.
스트리밍이란?
- 스트리밍은 route를 더 작은 "chunks"로 나누고 준비가 되면 서버에서 클라이언트로 점진적으로 스트리밍할 수 있는 데이터 전송 기술입니다.
- 스트리밍하면 느린 데이터 요청이 전체 페이지를 차단하는 것을 방지할 수 있습니다. 이를 통해 사용자는 UI가 사용자에게 표시되기 전에 모든 데이터가 로드될 때까지 기다리지 않고 페이지의 일부를 보고 상호 작용할 수 있습니다.
- 스트리밍이 하나의 청크로 간주되기때문에 리액트 컴포넌트와 잘 작동함.
next.js에서 스트리밍 사용방법
- loading.tsx추가
- Loading이란 이름으로 컴포넌트 만들기만 하면 로딩중에 자동으로 이걸 보여줌( 페이지 콘텐츠가 로드되는 동안 대체 UI로 표시할 폴백 UI를 생성할 수 있습니다.)
- Suspense 컴포넌트 사용
- loading.tsx추가
데이터를 받는 기능은 데이터가 필요한 컴포넌트로 옮기고 suspend 컴퍼넌트에 fallback으로 로딩 중 보여질 스켈레톤 컴포넌트를 전달
<Suspense fallback={<LatestInvoicesSkeleton />}>
<LatestInvoices />
</Suspense>
Search and Pagination
Why use URL search params?
- URL 매개변수를 사용하여 검색을 구현하면 다음과 같은 몇 가지 이점이 있습니다.
- 북마크 가능 및 공유 가능 URL : 검색 매개변수가 URL에 있으므로 사용자는 향후 참조 또는 공유를 위해 검색어 및 필터를 포함하여 애플리케이션의 현재 상태를 북마크에 추가할 수 있습니다.
- 서버 측 렌더링 및 초기 로드 : URL 매개변수를 서버에서 직접 사용하여 초기 상태를 렌더링할 수 있으므로 서버 렌더링을 더 쉽게 처리할 수 있습니다.
- 분석 및 추적 : URL에 직접 검색어와 필터가 있으면 추가 클라이언트 측 논리 없이도 사용자 행동을 더 쉽게 추적할 수 있습니다.
URL search params 추출하기 위한 방법
- 서버컴포넌트- searchParams을 prop으로 넘긴다.
- 클라이언트 컴포넌트 - useSearchParams를 사용한다.
//서버컴포넌트 export default async function Page({ searchParams, }: { searchParams?: { query?: string; page?: string; }; }) { const query = searchParams?.query || ''; const currentPage = Number(searchParams?.page) || 1; } //중략
//클라이언트 컴포넌트 import { useSearchParams, usePathname, useRouter } from 'next/navigation';
export default function Search() {
const searchParams = useSearchParams();
const pathname = usePathname();
const {replace} = useRouter();
//중략
}
### 검색 관련 clienthook
- useSearchParams
- 현재 URL의 매개변수에 액세스할 수 있습니다.
- 예를 들어, 이 URL '/dashboard/invoices?page=1&query=pending'에 대한 useSearchParams는 다음과 같습니다: {page: '1', query: 'pending'}.
- usePathname
- 현재 URL의 경로 이름을 읽을 수 있습니다.
- 예를 들어 /dashboard/invoices 경로의 경우, 사용 경로명은 '/dashboard/invoices'를 반환합니다.
- useRouter
- 클라이언트 구성 요소 내에서 프로그래밍 방식으로 경로 간 탐색을 활성화합니다. 여러 가지 방법을 사용할 수 있습니다.
### Debouncing
- 디바운싱이란
- 함수가 실행될 수 있는 속도를 제한하는 프로그래밍 방식입니다.
- 사용자가 입력을 중단한 경우에만 데이터베이스를 쿼리할 때 사용
- 디바운싱 작동 방식:
- Trigger Event : 디바운싱되어야 하는 이벤트(검색창의 키 입력 등)가 발생하면 타이머가 시작됩니다.
- 대기 : 타이머가 만료되기 전에 새로운 이벤트가 발생하면 타이머가 재설정됩니다.
- 실행 : 타이머가 카운트다운 끝에 도달하면 디바운싱된 함수가 실행됩니다.
- 예시
```javascript
import { useDebouncedCallback } from 'use-debounce';
// Inside the Search Component...
const handleSearch = useDebouncedCallback((term) => {
console.log(`Searching... ${term}`);
const params = new URLSearchParams(searchParams);
if (term) {
params.set('query', term);
} else {
params.delete('query');
}
replace(`${pathname}?${params.toString()}`);
}, 300);
Pagination
- invoices 페이지에서 전체 인보이스 검색후 전체 페이지 수를 Pagination 컴포넌트에 전달
- Pagination에서 각 페이지에 따른 url 생성-> 페이지 버튼에 link href로 전달 -> 각 페이지버튼 클릭시 page 연결
//page url 생성 함수 const createPageURL = (pageNumber: number | string) => { const params = new URLSearchParams(searchParams); params.set('page', pageNumber.toString()); return `${pathname}?${params.toString()}`; }; // 중략 <PaginationNumber key={page} href={createPageURL(page)} page={page} position={position} isActive={currentPage === page} />
Mutating Data
Server Actions?
- 서버에서 직접 비동기 코드를 실행.
- 데이터 변경 위해 API 엔드포인트 생성할필요 없음
- 보안에 효과적
- Next.js 캐싱과도 긴밀하게 통합 -> 서버 액션을 통해 양식이 제출되면 해당 액션을 사용하여 데이터를 변경할 수 있을 뿐만 아니라 revalidatePath 및 revalidateTag와 같은 API를 사용하여 관련 캐시의 유효성을 다시 검사할 수도 있습니다.
Using forms with Server Actions
// Server Component
export default function Page() {
// Action
async function create(formData: FormData) {
'use server';
// Logic to mutate data...
}
// Invoke the action using the "action" attribute
return <form action={create}>...</form>;
}
Create a Server Action
- 파일 최상단에 'use server'
- 파일 내에서 내보낸 모든 함수를 서버 함수로 표시할 수 있습니다.
- 서버 함수를 클라이언트 및 서버 컴포넌트로 가져올 수 있으므로 매우 다양하게 활용할 수 있습니다.
'use server'; export async function createInvoice(formData: FormData) { // formData에서 데이터 추출 const rawFormData = Object.fromEntries(formData.entries()) }
revalidatePath
- Next.js의 클라이언트 측 라우터 캐시가 사용자 브라우저에 일정 시간 동안 경로 세그먼트를 저장
- 이 캐시는 프리페칭과 함께 사용자가 서버에 대한 요청 횟수를 줄이면서 경로를 빠르게 탐색할 수 있도록 해줍니다.
- 데이터 업데이트 시 이 캐시를 지우고 서버에 대한 새 요청을 트리거해야한다.
- Next.js의 revalidatePath함수를 사용하면 캐시 지우고 새로 요청
- 요청후 redirect함수로 리다이렉트
'use server';
import { z } from 'zod';
import { sql } from '@vercel/postgres';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
// ...
export async function createInvoice(formData: FormData) {
// ...
revalidatePath('/dashboard/invoices');
redirect('/dashboard/invoices');
}
## Handling Errors
### not Found
[http://localhost:3000/dashboard/invoices/2e94d1ed-d220-449f-9f11-f0bbceed9645/edit]
- DB에 존재하지 않는 uuid 로 접속했을때 not Found 페이지로 연결됨
- not-found.tsx가 error.tsx보다 우선순위가 높아 구체적인 오류 처리할때 사용가능
```javascript
import { fetchInvoiceById, fetchCustomers } from '@/app/lib/data';
import { updateInvoice } from '@/app/lib/actions';
import { notFound } from 'next/navigation';
export default async function Page({ params }: { params: { id: string } }) {
const id = params.id;
const [invoice, customers] = await Promise.all([
fetchInvoiceById(id),
fetchCustomers(),
]);
if (!invoice) {
notFound();
}
// ...
}
Improving Accessibility
Form 접근성 개선
- Sementic HTML
- < div > 대신 시맨틱 요소(< input >, < option > 등)를 사용.
- Labelling
- < label > 과 htmlFor 사용 이렇게 하면 컨텍스트를 제공하여 AT 지원이 향상되고 사용자가 label을 클릭하여 해당 input필드에 초점을 맞출 수 있어 사용성이 향상됩니다.
- Focus Outline
- 필드가 초점이 맞춰져 있을 때 윤곽선이 표시
Form validation
client-side validation
- input , select요소에 required 속성 추가
server-side validation
- 고급 서버 측 유효성 검사를 위해 zod와 같은 라이브러리를 사용하여 데이터를 변경하기 전에 양식 필드의 유효성 검사가능
- DB에 데이터 보내기전에 예상된 형식인지 확인가능
- 악의적인 사용자가 클라이언트 측 유효성 검사를 우회하는 위험감소.
- 유효한 데이터로 간주되는 정보에 대한 하나의 진실 소스를 확보.
'use server'
import { z } from 'zod'
const schema = z.object({
email: z.string({
invalid_type_error: 'Invalid Email',
}),
})
export default async function createUser(formData: FormData) {
// zod 이용하여 검증
const validatedFields = schema.safeParse({
email: formData.get('email'),
})
// form data가 유효하지 않으면 바로 return
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
}
}
// formData의 validation 확인한 후에 Mutate data
}
- safeFarse는 sussess, errors포함한 객체를 반환해서, validation을 try catch 없이 진행할수 있게 한다.
- safeParse해서 검증되면, 검증된 filed의 데이타로뽑아냄..
- 서버에서 필드의 유효성을 검사하고 나면 액션에서 직렬화 가능한 객체를 반환하고 React useFormState 훅을 사용하여 사용자에게 메시지를 표시할 수 있습니다.
- 액션을 useFormState에 전달하면 액션의 함수 시그니처가 변경되어 첫 번째 인수로 새로운 prevState 또는 initialState 매개변수를 받습니다.
- useFormState는 React 훅이므로 클라이언트 컴포넌트에서 사용해야 합니다.
#### useFormState
- (action, initialState)를 받아 state, dispatch를 반환함.
```javascript
const [state, dispatch] = useFormState(createInvoice, initialState);
- 서버 액션과 함께 사용하는 경우, useFormState를 사용하면 하이드레이션이 완료되기 전에도 Form 제출에 대한 서버의 응답을 표시할 수 있음.
- state : form submit전에는 initial State , submit후에는 제공한 action(createInvoice)에 의한 return값
- dispatch : form 요소에 action 으로 전달할 새로운 action함수 .
< form action={ dispatch }>
- form의 action 속성에 dispatch 주입함.
- form이 submit되면 action함수(createInvoice)가 호출되어 state(currentState)를 반환함.
import { useFormState } from 'react-dom';
export default function Form({ customers }: { customers: CustomerField[] }) {
// 초기값은 message , errors를 속성으로 하는 객체
const initialState = { message: null, errors: {} };
// createInvoice 액션을 인수로 전달
const [state, dispatch] = useFormState(createInvoice, initialState);
// return <form action={createInvoice}>...</form>;
return <form action={dispatch}>...</form>;
}
// This is temporary until @types/react-dom is updated
export type State = {
errors?: {
customerId?: string[];
amount?: string[];
status?: string[];
};
message?: string | null;
};
export async function createInvoice(prevState: State, formData: FormData) {
// Validate form fields using Zod
// safeParse()는 성공 또는 오류 필드를 포함하는 객체를 반환합니다. 이렇게 하면 이 논리를 try/catch 블록 안에 넣지 않고도 유효성 검사를 보다 원활하게 처리하는 데 도움이 됩니다.
const validatedFields = CreateInvoice.safeParse({
customerId: formData.get('customerId'),
amount: formData.get('amount'),
status: formData.get('status'),
});
// If form validation fails, return errors early. Otherwise, continue.
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
message: 'Missing Fields. Failed to Create Invoice.',
};
}
// mutating data
}
aria-describedby
- aria-describedby 속성은 문자열로 구성된 고유한 id 값을 가져야 한다.
- 해당 Id 값은 추가 정보를 제공하는 id 속성과 일치해야 한다.
- 일반적으로 aria-describedby 속성을 사용하여 스크린 리더가 해당 요소에 대한 설명을 읽을 수 있도록 한다.
<select
id="customer"
name="customerId"
className="peer block w-full cursor-pointer rounded-md border border-gray-200 py-2 pl-10 text-sm outline-2 placeholder:text-gray-500"
defaultValue=""
aria-describedby="customer-error"
>
<!--중략-->
<div id="customer-error" aria-live="polite" aria-atomic="true">
{state.errors?.customerId &&
state.errors.customerId.map((error: string) => (
<p className="mt-2 text-sm text-red-500" key={error}>
{error}
</p>
))}
</div>
- 아이디 customer-error 갖는 에러설명 영역이 select 요소를 설명함. select에서 오류났을때 스크린리더가 customer-error에 있는 부분을 읽음
aria live
- 스크린리더가 페이지의 업데이트에 대해 알림
- aria-live="off"
- 실시간 업데이트 사용하지 않음
- aria-live="assertive"
- 변경 사항이 발생하면 가능한 빨리 읽음
- aria-live="polite"
- 변경사항 발생하면 사용자가 하던 작업이 끝나면 (사용자 활동이 없을때) 읽음
Authentication
Authentication vs. Authorization
- Authentication is about making sure the user is who they say they are. You're proving your identity with something you have like a username and password.
- Authorization is the next step. Once a user's identity is confirmed, authorization decides what parts of the application they are allowed to use
NextAuth.js
- 세션관리, 로그인-로그아웃,다른 인증관련 부분 포함한 복잡한 부분을 추상화함.
- (직접 구현하기에 오래걸리고, 에러나기쉬운) 인증 관한 통합 솔루션을 제공.
설치
- 라이브러리 설치
npm install next-auth@beta
- 암호화키 생성 및 보관
- 쿠키를 암호화하여 사용자 세션의 보안을 보장
openssl rand -base64 32
- 쿠키를 암호화하여 사용자 세션의 보안을 보장
- env파일에 AUTH_SECRET라는 변수로 저장
AUTH_SECRET=your-secret-key
- 페이지 옵션 추가
//auth.config.ts import type { NextAuthConfig } from 'next-auth';
export const authConfig = {
pages: {
signIn: '/login',
},
};
- 인증 설정에 있는 pages설정을 통해 login, logout, error페이지의 라우트 설정할수있다.
- 필수는 아니지만 이렇게 signIn에 '/login' 추가해주면 nextAuth 기본페이지 대신 자신이 설정한 login 페이지 볼수있음
4. middleware 설정
#### middleware.ts
```javascript
// middleware.ts
import NextAuth from 'next-auth';
import { authConfig } from './auth.config';
export default NextAuth(authConfig).auth;
export const config = {
// https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher
matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'], //이러한 경로에서 사용됨.
};
- 미들웨어에서 authConfig 객체를 사용해서 NextAuth.js를 초기화하고 인증 속성을 내보냄.
auth.config.ts
import type { NextAuthConfig } from 'next-auth';
export const authConfig = {
pages: {
signIn: '/login',
},
callbacks: {
authorized({ auth, request: { nextUrl } }) {
const isLoggedIn = !!auth?.user;
const isOnDashboard = nextUrl.pathname.startsWith('/dashboard');
if (isOnDashboard) {
if (isLoggedIn) return true;
return false; // Redirect unauthenticated users to login page
} else if (isLoggedIn) {
return Response.redirect(new URL('/dashboard', nextUrl));
}
return true;
},
},
providers: [], // Add providers with an empty array for now
} satisfies NextAuthConfig;
- authorized 콜백
- nextjs middleware로 검증되었는지 확인.
- login 안한상태에서 접근하는것 방지
- 다른요청보다 먼저 call되고, auth와 request 속성가진 객체 받음
- auth 속성은 사용자 세션을 포함하고, request는 들어온 request를 포함함.
- provider는 로그인 옵션 (google, github 등 )
- Credentials provider
- The Credentials provider allows users to log in with a username and a password.
- Credentials provider 사용하더라도 대체할 OAuth나 email provider를 제공하는 것이 좋다.
//auth.ts import NextAuth from 'next-auth'; import Credentials from 'next-auth/providers/credentials'; import { authConfig } from './auth.config'; import { z } from 'zod'; import { sql } from '@vercel/postgres'; import type { User } from '@/app/lib/definitions'; import bcrypt from 'bcrypt';
// db에서 user 검색해오기
async function getUser(email: string): Promise<User | undefined> {
try {
const user = await sqlSELECT * FROM users WHERE email=${email}
;
return user.rows[0];
} catch (error) {
console.error('Failed to fetch user:', error);
throw new Error('Failed to fetch user.');
}
}
export const { auth, signIn, signOut } = NextAuth({
...authConfig,
providers: [
Credentials({
// authentication 로직을 다루기위해 authorize 함수사용.
async authorize(credentials) {
// zod 로 검증
const parsedCredentials = z
.object({ email: z.string().email(), password: z.string().min(6) })
.safeParse(credentials);
if (parsedCredentials.success) {
const { email, password } = parsedCredentials.data;
const user = await getUser(email);
if (!user) return null;
const passwordsMatch = await bcrypt.compare(password, user.password); // 비밀번호 맞는지 체크
if (passwordsMatch) return user;//패스워드와 일치하면 user 반환
}
return null;
},
}),
],
});
6. Login
- action 함수 생성
```javascript
//action.ts
import { signIn } from '@/auth';
import { AuthError } from 'next-auth';
// ...
export async function authenticate(
prevState: string | undefined,
formData: FormData,
) {
try {
await signIn('credentials', formData);
} catch (error) {
if (error instanceof AuthError) {
switch (error.type) {
case 'CredentialsSignin':
return 'Invalid credentials.';
default:
return 'Something went wrong.';
}
}
throw error;
}
}
- login form에 useFormState사용해서 서버액션 콜, error 핸들 추가 기능 추가
import { useFormState, useFormStatus } from 'react-dom'; import { authenticate } from '@/app/lib/actions';
export default function LoginForm() {
const [errorMessage, dispatch] = useFormState(authenticate, undefined);
return (