ChamomileGuides 3.0.4 Help

렌더러 & 라우트

3.1 Render

Next.js 렌더링

Next.js에서 지원하는 다양한 렌더링 방식에 대한 설명입니다. 각 렌더링 방식은 상황에 맞게 사용될 수 있으며, 성능 및 사용자 경험에 영향을 미칩니다.

  • SEO 적용이 크게 중요하지 않거나 데이터 pre-rendering이 필요없다면 CSR

  • 매 요청마다 화면이 달라지면서 서버 사이드로 렌더링을 하고자 한다면 SSR

  • 정적 문서로 충분한 화면이면서 빠른 HTML 문서 반환이 필요하다면 SSG

  • 컨텐츠가 동적이지만 자주 변경되지 않는 경우 ISR

SSR (Server Side Rendering)

SSR은 사용자가 요청할 때마다 서버에서 HTML을 생성하고 클라이언트에 전송하는 방식입니다. 초기 로딩 시 SEO와 최신 데이터를 요구하는 페이지에 적합합니다.

주요 특징:

  • 서버에서 HTML을 생성하여 클라이언트에 전달

  • 초기 페이지 로드 시 완전한 HTML 제공

  • SEO에 유리

  • 페이지 요청 시마다 서버에서 렌더링 수행

사용 예:

export default async function Page() { const data = await fetch('https://api.example.com/data').then((res) => res.json()); return <div>{data.content}</div>; }

CSR (Client Side Rendering)

CSR은 서버에서 전달된 기본 HTML에 JavaScript를 로드하여 브라우저에서 동적으로 콘텐츠를 렌더링하는 방식입니다. 사용자와 상호작용이 많은 페이지에서 적합합니다.

주요 특징:

  • 첫 페이지 로드 시 빈 HTML이 전송되며, 이후 JavaScript가 데이터를 로드합니다.

  • 초기 로드가 빠르지만, 콘텐츠 표시까지 시간이 걸릴 수 있습니다.

  • 파일 상단에 'use client;'를 적어줍니다.

사용 예:

'use client'; import { useEffect, useState } from 'react'; export default function Page() { const [data, setData] = useState(null); useEffect(() => { fetch('https://api.example.com/data') .then((res) => res.json()) .then(setData); }, []); if (!data) return <div>Loading...</div>; return <div>{data.content}</div>; }

SSG (Static Site Generation)

SSG는 빌드 시점에 HTML을 생성하여 CDN에 배포하는 방식입니다. 데이터가 자주 변하지 않는 정적 콘텐츠에 적합합니다.

주요 특징:

  • HTML이 빌드 시에 생성되므로 매우 빠른 로딩 속도를 제공합니다.

  • 정적 파일로 배포되므로 서버 부하가 적습니다.

  • SEO에 유리합니다

사용 예:

export async function generateStaticParams() { const data = await fetch('https://api.example.com/paths'); return data.map((item) => ({ slug: item.slug })); } export default function Page({ params }) { return <div>Slug: {params.slug}</div>; }

ISG (Incremental Static Generation)

SSR은 사용자가 요청할 때마다 서버에서 HTML을 생성하고 클라이언트에 전송하는 방식입니다. 초기 로딩 시 SEO와 최신 데이터를 요구하는 페이지에 적합합니다.

주요 특징:

  • SSG와 유사하게 빌드 시점에 HTML을 생성

  • 특정 간격으로 페이지를 재생성하여 최신 데이터 반영

  • 성능이 뛰어나면서도 최신 데이터 제공 가능

사용 예:

export async function generateMetadata() { return { revalidate: 60 }; // 60초마다 재생성 } export default async function Page() { const data = await fetch('https://api.example.com/data', { next: { revalidate: 60 } }); const json = await data.json(); return <div>{json.content}</div>; }

3.2 Route

Next.js 라우팅

Next.js 13버전 이상에서 사용되는 App Router의 라우팅 방법을 소개합니다.

용어

컴포넌트 트리 이미지
  • 트리: 계층적 구조를 시각화하기 위한 규칙. 예를 들어, 부모 및 자식 컴포넌트가 있는 컴포넌트 트리, 폴더 구조 등.

  • 서브트리: 새로운 루트(첫 번째)에서 시작하여 리프(마지막)에서 끝나는 트리의 일부.

  • 루트: 트리 또는 서브트리의 첫 번째 노드, 예를 들어 루트 레이아웃.

  • 리프: 자식이 없는 서브트리의 노드, 예를 들어 URL 경로의 마지막 세그먼트.

URL 세그먼트
  • URL 세그먼트: 슬래시로 구분된 URL 경로의 일부.

  • URL 경로: 도메인 이후에 오는 URL의 일부로, 세그먼트로 구성됨.

경로 생성하기

Next.js는 파일 시스템 기반의 라우터를 사용하여 폴더를 경로 정의에 사용합니다.

각 폴더는 URL 세그먼트에 매핑되는 경로 세그먼트를 나타냅니다.

라우트 세그먼트

page.tsx 파일은 경로 세그먼트를 공개적으로 접근할 수 있도록 합니다.

경로 정의하기

이 예에서, /dashboard/analytics URL 경로는 해당 page.tsx 파일이 없기 때문에 공개적으로 접근할 수 없습니다. 이 폴더는 컴포넌트, 스타일시트, 이미지 또는 다른 콜로케이트 파일을 저장하는 데 사용할 수 있습니다.

파일 규칙

Next.js는 중첩 경로에서 특정 동작을 가진 UI를 생성하기 위해 일련의 특수 파일을 제공합니다:

layout

세그먼트와 자식에 대한 공유 UI

page

경로의 고유한 UI로 경로를 공개적으로 접근 가능하게 만듭니다.

loading

세그먼트와 자식에 대한 로딩 UI

not-found

세그먼트와 자식에 대한 Not Found UI

error

세그먼트와 자식에 대한 오류 UI

global-error

글로벌 오류 UI

route

서버 사이드 API 엔드포인트

template

특수한 재렌더링된 레이아웃 UI

default

[병렬 경로]에 대한 폴백 UI

컴포넌트 계층 구조

특수 파일에 정의된 React 컴포넌트는 특정 계층 구조로 렌더링됩니다:

  • layout.js

  • template.js

  • error.js (React 오류 경계)

  • loading.js (React 서스펜스 경계)

  • not-found.js (React 오류 경계)

  • page.js 또는 중첩 layout.js

파일 규칙을 위한 컴포넌트 계층 구조

중첩 경로에서는 세그먼트의 컴포넌트가 부모 세그먼트의 컴포넌트 내부에 중첩됩니다.

중첩 파일 규칙 컴포넌트 계층 구조

3.3 Fetch

Next.js Data Fetching

Next.js 13부터 도입된 App Router는 서버 컴포넌트와 클라이언트 컴포넌트를 명확히 구분하여 사용할 수 있게 해줍니다. 이로 인해 데이터 페칭과 캐싱 전략도 각각 다르게 적용할 수 있습니다. 아래는 서버 컴포넌트와 클라이언트 컴포넌트에서 데이터 페칭과 캐싱을 다루는 방법에 대한 설명입니다.

서버 컴포넌트에서 데이터 페칭 및 캐싱

서버 컴포넌트는 서버에서 렌더링되며, 데이터 페칭을 위해 fetch 함수를 사용할 수 있습니다. 서버 컴포넌트는 클라이언트에게 HTML을 반환하기 전에 데이터를 가져올 수 있기 때문에, 초기 로딩 성능이 향상됩니다.

fetch 함수 사용 예시

export default async function Page() { const res = await fetch('https://api.example.com/data'); const data = await res.json(); return ( <div> <h1>Data from API</h1> <pre>{JSON.stringify(data, null, 2)}</pre> </div> ); }

캐싱

Next.js의 fetch 함수는 기본적으로 캐싱을 지원합니다. fetch 함수의 두 번째 인자로 캐싱 옵션을 설정할 수 있습니다. 예를 들어, cache 옵션을 사용하여 캐싱 동작을 제어할 수 있습니다.

export default async function Page() { const res = await fetch('https://api.example.com/data', { cache: 'force-cache', // 캐시를 강제로 사용 }); const data = await res.json(); return ( <div> <h1>Data from API</h1> <pre>{JSON.stringify(data, null, 2)} </pre> </div> ); }

캐싱 옵션

  • no-store: 항상 최신 데이터를 가져옵니다.

  • force-cache: 캐시된 데이터를 사용하고, 없으면 네트워크 요청을 합니다.

  • only-if-cached: 캐시된 데이터가 있을 때만 사용합니다.

클라이언트 컴포넌트에서 데이터 페칭 및 캐싱

클라이언트 컴포넌트는 브라우저에서 실행되며, 데이터 페칭을 위해 axios와 같은 라이브러리를 사용할 수 있습니다. 클라이언트 컴포넌트에서는 SWR이나 TanStack Query와 같은 라이브러리를 사용하여 캐싱을 관리할 수 있습니다

TanStack Query를 사용한 데이터 페칭 및 캐싱 예시

'use client'; import { useQuery } from '@tanstack/react-query'; import axios from 'axios'; const fetchData = async () => { const { data } = await axios.get('https://api.example.com/data'); return data; }; export default function ClientComponent() { const { data, error, isLoading } = useQuery(['data'], fetchData); if (isLoading) { return <div>Loading...</div>; } if (error) { return <div>Error loading data</div>; } return ( <div> <h1>Data from API</h1> <pre>{JSON.stringify(data, null, 2)}</pre> </div> ); }

3.4 State (Server)

서버 상태

  • Next.js 애플리케이션에서 서버 상태 관리는 클라이언트와 서버 간의 데이터 동기화를 효율적으로 처리하는 데 중요한 역할을 합니다. 서버 상태는 서버에서 관리되는 데이터로, 클라이언트에서 직접 수정할 수 없으며 서버와의 통신을 통해서만 변경할 수 있습니다

서버 상태 관리의 중요성

  • 데이터 일관성 유지: 서버 상태 관리를 통해 클라이언트와 서버 간의 데이터 일관성을 유지할 수 있습니다.

  • 성능 최적화: 서버 상태를 효율적으로 관리하면 불필요한 네트워크 요청을 줄이고, 캐싱을 통해 성능을 최적화할 수 있습니다.

  • 사용자 경험 향상: 서버 상태를 적절히 관리하면 사용자에게 더 빠르고 일관된 경험을 제공할 수 있습니다.

서버 상태 관리 도구

1. SWR (Stale-While-Revalidate)

SWR은 Vercel에서 개발한 React Hooks 라이브러리로, 데이터 페칭을 간단하게 만들고 캐싱을 자동으로 관리해줍니다. SWR은 "Stale-While-Revalidate" 전략을 사용하여 빠른 데이터 로딩과 최신 데이터 동기화를 동시에 처리합니다.

주요 기능

  • 자동 캐싱 및 재검증: 데이터가 오래되면 자동으로 재검증합니다.

  • 포커스 재검증: 사용자가 브라우저 탭을 다시 활성화할 때 데이터를 자동으로 재검증합니다.

  • 폴링: 주기적으로 데이터를 갱신할 수 있습니다.

  • 사용 예시

import axios from 'axios'; import useSWR from 'swr'; const fetcher = (url) => axios.get(url).then((res) => res.data); export default function ExampleComponent() { const { data, error } = useSWR('/api/data', fetcher); if (error) return <div>Error loading data</div>; if (!data) return <div>Loading...</div>; return <div>Data: {JSON.stringify(data)}</div>; }

2. TanStack Query (React Query)

TanStack Query는 서버 상태 관리를 위한 강력한 라이브러리로, 데이터 페칭, 캐싱, 동기화 및 서버 상태 관리를 간편하게 처리할 수 있습니다.

주요 기능

  • 자동 캐싱 및 백그라운드 데이터 갱신: 데이터를 자동으로 캐싱하고 백그라운드에서 갱신합니다.

  • 쿼리 무효화: 특정 조건에서 쿼리를 무효화하고 데이터를 다시 가져올 수 있습니다.

  • 병렬 및 의존성 쿼리: 병렬로 여러 쿼리를 실행하거나, 특정 쿼리가 완료된 후 다른 쿼리를 실행할 수 있습니다.

  • 사용 예시

import { useQuery } from '@tanstack/react-query'; import axios from 'axios'; const fetchData = async () => { const { data } = await axios.get('/api/data'); return data; }; export default function ExampleComponent() { const { data, error, isLoading } = useQuery(['data'], fetchData); if (isLoading) return <div>Loading...</div>; if (error) return <div>Error loading data</div>; return <div>Data: {JSON.stringify(data)}</div>; }

3.5 State (Client)

클라이언트 상태

  • 클라이언트 상태 관리는 사용자 인터페이스와 관련된 상태를 관리하는 데 중요한 역할을 합니다. 클라이언트 상태는 서버와의 통신 없이도 클라이언트 측에서 직접 관리되고 변경될 수 있는 상태를 의미합니다.

  • 각 컴포넌트에서는 지역 상태 (useState, useRef )로 상태를 관리합니다.

  • 전역 상태를 관리하기 위해서 기본 제공되는 React Context API를 사용할수 있고, 다양한 상태관리 라이브러리(zustand, jotai, recoil, redux )를 사용할 수 도있습니다.

클라이언트 상태 관리의 중요성

  • 상태 일관성 유지: 클라이언트 상태 관리를 통해 컴포넌트 간의 상태 일관성을 유지할 수 있습니다.

  • 성능 최적화: 불필요한 렌더링을 줄이고, 필요한 부분만 업데이트하여 성능을 최적화할 수 있습니다.

  • 코드 구조 개선: 상태 관리를 중앙화하여 코드의 가독성과 유지보수성을 높일 수 있습니다.

클라이언트 상태 관리 도구

1. Context API

React Context API는 프로젝트의 상위/하위 컴포넌트 간 데이터 공유 방식입니다. useContext는 React의 내장 훅으로, 컴포넌트 트리 전체에 데이터를 전역적으로 전달할 수 있게 해줍니다. 간단한 상태 관리에 적합하며, 별도의 라이브러리 설치가 필요 없습니다.

주요 특징

  • 간단하고 직관적: 기본적인 전역 상태 관리에 적합합니다.

  • 내장 기능: 별도의 라이브러리 설치가 필요 없습니다.

  • 성능 문제: 상태가 변경될 때마다 모든 하위 컴포넌트가 다시 렌더링될 수 있습니다.

  • React에서 발생하는 문제점인 Props drilling을 해결하기 위한 수단입니다.

props drilling
import React, { createContext, useContext } from 'react'; const MyContext = createContext('Hello, world'); // 1 export default function MyComponent() { ​​return <ChildComponent />; } export function ChildComponent() { ​​const value = useContext(MyContext); // 2 ​​return <p>{value}</p>; }

2. Zustand

Zustand는 간단하고 직관적인 API를 제공하는 상태 관리 라이브러리로, 작은 크기와 빠른 성능을 자랑합니다. 전역 상태를 쉽게 관리할 수 있게 해줍니다.

주요 특징

  • typescript로 작성되었습니다.

  • redux를 축소화시킨 느낌으로 redux와 유사합니다.

  • provider가 필요없어 상태 변경 시 불필요한 리랜더링을 최소화합니다.

  • 보일러플레이트가 거의 없습니다.

  • 한 개의 중앙에 집중된 형식의 스토어 구조를 활용하면서, 상태를 정의하고 사용하는 방법이 단순합니다.

  • Flux 패턴으로 동작합니다.

flux
import create from 'zustand'; const useStore = create((set) => ({ count: 0, increaseCount: () => set((state) => ({ count: state.count + 1 })), setThree: (input) => set({ count: input }), }));
function App() { const { count, increaseCount, setThree } = useStore(); return ( <div className="App"> <div>Zustand ! {count}</div> <button onClick={increaseCount}>+1</button> <button onClick={() => setNum(3)}>set3</button> </div> ); }

3.6 Configs

Next.js 프로젝트의 루트 폴더에 위치한 설정 파일들에 대한 설명입니다. 이 파일들은 프로젝트의 빌드, 스타일링, 코드 포맷팅, 타입스크립트 설정 등을 관리합니다.

next.config.mjs

next.config.mjs 파일은 Next.js 설정을 정의합니다.

/** @type {import('next').NextConfig} */ // Next.js 설정 파일을 정의합니다. const nextConfig = { // React의 엄격 모드를 비활성화합니다. reactStrictMode: false, // 비동기적으로 rewrites 설정을 정의합니다. // CORS 문제를 해결하기 위한 프록시 설정을 포함합니다. async rewrites() { return [ { // '/chmm/:path*' 경로로 들어오는 요청을 // NEXT_PUBLIC_API_URL_BASE 환경 변수로 설정된 기본 URL을 사용하여 프록시합니다. source: '/chmm/:path*', destination: `${process.env.NEXT_PUBLIC_API_URL_BASE}/chmm/:path*`, }, ]; }, // Webpack 설정을 커스터마이징합니다. webpack(config) { // .svg 파일을 처리하기 위한 규칙을 추가합니다. // @svgr/webpack 로더를 사용하여 SVG 파일을 React 컴포넌트로 변환합니다. config.module.rules.push({ test: /\.svg$/, use: ['@svgr/webpack'], }); // 수정된 Webpack 설정을 반환합니다. return config; }, }; // nextConfig 객체를 기본 내보내기로 설정합니다. export default nextConfig;

tsconfig.json

tsconfig.json 파일은 TypeScript 설정을 정의합니다.

postcss.config.js

postcss.config.js 파일은 PostCSS 설정을 정의합니다. PostCSS는 CSS를 변환하는 도구로, 다양한 플러그인을 통해 CSS를 처리할 수 있습니다. 주로 Tailwind CSS와 같은 도구와 함께 사용됩니다.

prettier.config.js

prettier.config.js 파일은 Prettier 설정을 정의합니다. Prettier는 코드 포맷터로, 일관된 코드 스타일을 유지하는 데 도움을 줍니다.

tailwind.config.js

tailwind.config.js 파일은 Tailwind CSS 설정을 정의합니다. Tailwind CSS는 유틸리티 퍼스트 CSS 프레임워크로, 빠르게 스타일링을 할 수 있게 해줍니다.

3.7 Env

Next.js 환경변수(env)

Next.js에서 환경 변수는 어플리케이션을 구성하는 동안 또는 어플리케이션이 실행 중일 때 사용되는 외부 구성 옵션입니다. 이러한 변수들은 보안 정보(예: API 키), 연결 설정, 플래그 및 다양한 실행 환경에 대한 세부 정보와 같은 중요한 데이터를 저장하는 데 사용됩니다. 환경 변수의 사용은 소스 코드 내에 구성 데이터를 하드코딩하는 것을 방지하고, 보안을 강화하며, 개발 및 프로덕션 환경 간에 쉽게 전환할 수 있는 능력을 제공합니다.

환경 변수를 사용해야 하는 이유

  1. 보안: 민감한 정보가 버전 제어 시스템에 노출되지 않아 응용 프로그램을 잠재적인 위협으로부터 보호할 수 있습니다.

  2. 환경 구분: 다양한 환경(예: 개발, 생산)에 대해 코드베이스를 변경하지 않고도 다른 설정을 사용할 수 있습니다. 이를 통해 쉬운 전환과 일관된 성능을 달성할 수 있습니다.

  3. 코드 이식성 및 깔끔함: 구성 세부 사항이 내장되지 않은 코드는 더 깔끔하고 논리에 집중됩니다. 이로 인해 코드의 가독성이 향상되고 동일한 코드베이스를 다른 환경에서 다른 구성으로 사용할 수 있어 이식성이 높아집니다.

  4. 변경의 용이성: 비밀 또는 구성이 변경된 경우 코드를 변경하고 응용 프로그램을 다시 배포하는 것보다 환경 변수를 업데이트하는 것이 훨씬 쉽고 안전합니다.

환경 변수 사용 방법

환경별 파일 분리

각각 로컬 / 개발 / 운영 환경에서 사용할 환경변수를 다른 파일로 분리하여 관리합니다

  • .env.local

  • .env.dev

  • .env.prod

브라우저에 변수 노출

브라우저에서 사용되는 변수는 NEXT_PUBLIC_ 접두사를 붙여야 합니다. 그러나 이러한 변수는 공개적으로 접근할 수 있으므로 민감한 정보를 담지 않아야 합니다.

환경 변수 사용

process.env 네임스페이스를 사용하여 액세스할 수 있습니다

const res = await fetch(`${process.env.NEXTAUTH_URL}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: credentials?.username, // 비밀번호를 RSA로 암호화 password: encRSA(credentials?.password as string, `${process.env.RSA_PUBLIC_KEY}`), }), });
if (process.env.NEXT_PUBLIC_COPY_CUT === 'true') { // 복사 붙여넣기 동작 처리 }

.env.local 예시

AUTH 관련한 설정과, 서버 URL 그리고 브라우저에서 노출되는 NEXT_PUBLIC 접두어를 가진 서버 URL, RECAPTCHA KEY, COPY & CUT, 루트 메뉴이름 옵션등이 존재합니다.

#AUTH NEXTAUTH_URL=http://devserver:8080/security/jwt/authenticate NEXTAUTH_REFRESH_URL=http://devserver:8080/security/jwt/refresh-token NEXTAUTH_REFRESH_TIME=300 NEXTAUTH_SECRET="n/dVaLz/hYv6wo5xxBCSECRETKEYkkkk" RSA_PUBLIC_KEY="PUBLICKEYkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7n9T9y2rflLzbudMjm+PslBSQTGGCPaySE5ayghbr6lRZ4QDA+WTf2SDxjBWh91J7C2NkTQ6PUBLICKEYCNOBgsQaO7Q0Sh5nrl34XSVrq53hMIHC7OmP98Wqjqdz6c6BQFicyefGZffOtyG4eBjWtu0oAd0/wW902X0sVnzn/g30V6PUBLICKEYov8Nq49W2F7g1goh1IMPUBLICKEY1259vviV1ONOsqyzmrDY+q9kkEW6bXckZH53Soihhgf+bUA36qagV7EPUBLICKEYDVvRxGpteNWkVbr3ybQIDAQAB" #URL NEXT_PUBLIC_API_URL_BASE=http://devserverpublic:8080 #ReCaptcha NEXT_PUBLIC_RECAPTCHA_SITE_KEY="6Lc9xRECAPTCHAs2OVHCjDoDRECAPTCHA0zPTjVym" #Copy & Cut NEXT_PUBLIC_COPY_CUT=false #Menu NEXT_PUBLIC_ROOT_MENU_ID=menu00000113

3.8 Auth

Auth - next-auth

NextAuth는 Next.js 애플리케이션에서 인증을 쉽게 구현할 수 있도록 도와주는 라이브러리입니다. 다양한 인증 제공자(Google, Facebook, GitHub 등)를 지원하며, 사용자 정의 인증 로직도 쉽게 추가할 수 있습니다.

  • next-auth가 5버전 부터는 auth.js로 이름이 변경되었는데, 아직 불안정하여 4.24.10 버전을 사용하였습니다.

auth.ts

  • auth.ts 모든 인증 관련 로직을 처리하는 핵심 파일입니다.

Providers

  • providers는 사용자가 로그인할 때 사용할 수 있는 인증 제공자를 정의합니다. NextAuth는 다양한 제공자를 지원하며, 각 제공자는 고유한 설정이 필요합니다.

  • 환경변수 NEXTAUTH_SECRET를 선언하여 토큰 정보를 encrypt, decrypt할때 사용합니다.

  • encRSA 함수를 이용하여 비밀번호를 RSA 암호화합니다.

  • 환경변수 NEXTAUTH_URL로 Chamomile 로그인 API를 호출합니다.

  • response로 받은 accessToken, refreshToken을 반환합니다.

secret: process.env.NEXTAUTH_SECRET, // 인증에 사용할 비밀 키 session: { strategy: 'jwt' }, // 세션 전략을 JWT로 설정 providers: [ CredentialsProvider({ name: 'Credentials', credentials: { username: { label: 'Username', type: 'text' }, password: { label: 'Password', type: 'password' }, }, async authorize(credentials): Promise<User | null> { const res = await fetch(`${process.env.NEXTAUTH_URL}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: credentials?.username, // 비밀번호를 RSA로 암호화 password: encRSA(credentials?.password as string, `${process.env.RSA_PUBLIC_KEY}`), }), }); if (!res.ok) { console.error('로그인 요청 실패:', res.statusText); return null; } const json = await res.json(); // json.data에서 accessToken과 refreshToken 추출 const { accessToken, refreshToken } = json.data; return { id: credentials?.username as string, accessToken, refreshToken }; }, }), ],

Callbacks - jwt

  • jwt 콜백은 JSON Web Token(JWT)을 생성하거나 갱신할 때 호출됩니다

  • 로그인 이후에 user정보와, 만료시간을 token에 저장해줍니다

  • refresh token 강제 갱신 trigger 발생시에 토큰 재발급 함수(getNewRefreshToken)를 호출합니다.

  • 환경변수 NEXTAUTH_REFRESH_TIME 에 정의된 시간(기본 300초) 이하로 만료시간이 남으면, 토큰 재발급 함수(getNewRefreshToken)를 호출합니다.

async jwt({ token, user, trigger }) { // user는 최초 로그인 시에만 존재함 // trigger는 client에서 갱신요청(update)시에 사용 const getExpireDateTime = (accessToken: string) => { const decoded = jwtDecode<{ exp: number }>(accessToken); // Date 객체로 변환 (밀리초 단위로 변환 필요) 후 ISO 8601 형식으로 변환 return new Date(decoded.exp * 1000).toISOString(); }; if (user) { // 로그인 시 JWT에 토큰 저장 token.accessToken = user.accessToken; token.refreshToken = user.refreshToken; token.name = user.id; token.expireDateTime = getExpireDateTime(token.accessToken as string); return token; } const decoded = jwtDecode<{ exp: number }>(token.accessToken as string); const currentTime = Math.floor(Date.now() / 1000); const refreshTime = Number(process.env.NEXTAUTH_REFRESH_TIME); // 만료 임박 기준 (300초) // console.log('남은 토큰 유효시간 🥜', decoded.exp - currentTime - refreshTime); // 리프레시 시간 체크 if (decoded.exp - currentTime <= refreshTime || trigger === 'update') { try { const { accessToken, refreshToken } = await getNewRefreshToken( token.refreshToken as string ); // 새로 토큰으로 저장 token.accessToken = accessToken; token.refreshToken = refreshToken; token.expireDateTime = getExpireDateTime(accessToken); } catch (error) { console.error('리프레시 토큰 갱신 중 에러 발생:', error); token.error = 'RefreshAccessTokenError'; } } return token; },

Callbacks - session

  • session 콜백은 세션이 생성되거나 갱신될 때 호출됩니다.

  • 브라우저에서 useSession을 호출할때 사용되며, 브라우저에서 노출되는 세션 객체를 반환합니다.

// 세션 콜백: 클라이언트로 데이터 전달 async session({ session, token }) { session.accessToken = token.accessToken as string; // 세션에 accessToken 저장 session.expireDateTime = token.expireDateTime as string; // 세션에 expireDateTime 저장 session.error = token.error as string; return session; },

gnb-info

  • handleRefreshToken함수에서 update()를 호출하여 auth.ts의 jwt함수의 trigger로 'update' 값을 전달하여 토큰 재발급 합니다.

  • signOut을 호출하여 세션을제거하고 로그아웃합니다.

import { signOut, useSession } from 'next-auth/react'; const { data: session, update } = useSession(); const handleLogout = async () => { signOut({ redirect: false }).then(() => { router.replace(`/${params.lng}/login`); }); }; const handleRefreshToken = (event: React.MouseEvent<HTMLDivElement>) => { event.preventDefault(); update(); };

middleware

  • middleware에서 매 요청시 토큰을 확인하고, 토큰이 있으면 그대로 반환, 토큰이 없으면 login 페이지로 redirect합니다.

// JWT 토큰을 가져옵니다 const token = await getToken({ req: req, secret: process.env.NEXTAUTH_SECRET, }); // 토큰이 없고, 요청된 경로가 `/login`이 아닌 경우 로그인 페이지로 리다이렉트합니다. if (!token && !/\/\w+\/login/.test(req.nextUrl.pathname)) { return NextResponse.redirect(new URL(`/${lng}/login`, req.url)); }

3.9 i18n

i18n(internationalization)

Next.js에서 다국어를 제공하는 방법에 대한 내용입니다.

react-i18next

  • react-i18next는 React 애플리케이션에서 국제화(i18n)를 쉽게 구현할 수 있도록 도와주는 라이브러리입니다.

  • react-i18next의 app router에 적용하는 방법을 참고하여 적용하였습니다.
    i18next app router 공식 가이드

  • 사용 라이브러리

다국어 처리

  • 선택된 언어는 url의 첫번째 세그먼트([lng])에 위치합니다.

  • 예시) chamomile.com/ko /login, chamomile.com/en /login

middleware.ts

middleware에서 라우팅 기반의 언어를 설정, 예외처리 합니다.

  • 언어 설정: acceptLanguage.languages(languageList);를 통해 지원하는 언어 목록을 설정합니다. 클라이언트의 쿠키에서 언어 설정을 가져오고(req.cookies.get(cookieName)), 쿠키에 설정된 언어가 없으면 Accept-Language 헤더에서 언어를 가져옵니다. 쿠키와 헤더 모두에서 언어를 찾지 못하면 기본 언어(fallbackLng)를 사용합니다.

  • 지원하지 않는 언어 경로 리다이렉션: 요청된 경로가 지원하는 언어 목록(languageList)에 포함되지 않으면, 기본 언어 경로로 리다이렉트합니다. 예를 들어, /es/some-path가 지원되지 않는 경우, /en/some-path로 리다이렉트합니다.

  • 참조(referer) 기반 언어 설정 :요청 헤더에 referer가 있는 경우, 참조 URL에서 언어를 추출하여 쿠키에 저장합니다. 이를 통해 사용자가 이전에 방문한 페이지의 언어 설정을 유지할 수 있습니다.

language-selector.tsx

LanguageSelector 컴포넌트에서는 지원하는 언어를 렌더링하며, 언어를 선택할 시에 라우팅 변경하여 다국어를 선택할 수 있게 합니다.

/src/app/i18n/locales 폴더

언어 번역 json파일이 위치하는 폴더입니다.
/locales/[lng]/[namespace].json 형태로 존재합니다.

  • ko(한국어)

  • en(영어)

  • ja(일본어)

  • zh(중국어)

  • vi(베트남어)

  • it(이탈리아어)

client.ts

  • i18next 설정 및 React용 useTranslation 커스텀 훅

  • i18next를 초기화하고, 백엔드 및 로컬 번역 리소스를 병합하여 다국어 지원을 제공합니다.

import { useFetchMessages } from '@/hooks/useFetchMessages'; const { messageResult, isMessageLoading } = useFetchMessages(lng); // 백엔드 메시지 가져오기 /** * NOTE: 백엔드 메시지 로드 및 병합 * 백엔드에서 가져온 번역 리소스를 로컬 JSON 파일과 병합 */ useEffect(() => { if (messageResult?.data) { const backendMessages = messageResult.data.reduce( (acc: any, { code, message }: { code: string; message: string }) => { acc[code] = message; return acc; }, {} ); // 로컬 JSON 파일 로드 후 병합 import(`./locales/${lng}/${ns}.json`).then((localTranslations) => { const mergedTranslations = mergeTranslations(localTranslations.default, backendMessages); // i18n에 병합된 번역 리소스를 추가 i18n.addResourceBundle(lng, ns, mergedTranslations, true, true); setIsBackendLoaded(true); // 백엔드 데이터 로드 완료 표시 }); } }, [messageResult, lng, i18n, ns]);

페이지(컴포넌트)에서 사용 예시

  • useTransaction 훅에 언어코드(params.lng )와 네임스페이스(namespace )를 설정합니다.

  • 메시지에 변수 삽입 (interpolation)

    메시지에 key({0}, {1}, {userName} 등)가 포함되어 있으면 변수를 삽입할 수 있습니다.

import { useTranslation } from '@/app/i18n/client'; const params = useParams<{ lng: string }>(); const { t } = useTranslation(params.lng, 'common'); function onLoginClick() { const MAX_LOGIN_TRY_COUNT = 5; let loginTryCount = 3; /* * message.password.mismatch: 비밀번호가 틀렸습니다. ({0}/{1}회) * * → 비밀번호가 틀렸습니다. (3/5회) */ alert(t('message.password.mismatch', { '0': loginTryCount, '1': MAX_LOGIN_TRY_COUNT })); } return ( <Button className="mt-14 w-full" type="submit" onClick={onLoginClick} > {t('login.login')} </Button> )

3.10 Typescript

TypeScript는 정적 타입을 제공하여 코드의 안정성과 가독성을 높여줍니다.

타입 정의 위치 및 규칙

  • 타입 정의는 src/types/ 폴더 아래에 파일을 생성하여 관리합니다. menu.ts, user.ts 등의 파일을 생성하여 각 파일에 타입을 정의합니다.

  • 각 컴포넌트 내에서만 사용하는 타입은 컴포넌트 상단에 정의하여 사용합니다.

  • Typescript 컨벤션

  • Props 컨벤션

주요 타입

원시 타입 (Primitive Types)

  • string: 문자열 타입

  • number: 숫자 타입

  • boolean: 불리언 타입

  • null: 널 타입

  • undefined: 정의되지 않은 타입

React 컴포넌트 관련 타입

  • ReactNode: 렌더링 가능한 모든 타입 (JSX, 문자열, 숫자 등)

이벤트 핸들러 타입

  • MouseEvent: 마우스 이벤트 타입

  • ChangeEvent: 입력 값 변경 이벤트 타입

const handleUsername: ChangeEventHandler<HTMLInputElement> = (e) => { setUsername(e.target.value); };
  • KeyboardEvent: 키보드 이벤트 타입

  • FormEvent: 폼 이벤트 타입

클릭 핸들러에서 사용하는 타입

  • MouseEvent<HTMLButtonElement>: 버튼 클릭 이벤트 타입

const handleRefreshToken = (event: React.MouseEvent<HTMLDivElement>) => { event.preventDefault(); update(); };

상태 관리 관련 타입

  • SetStateAction<타입>: setState 함수의 인자 타입

  • Dispatch<SetStateAction<타입>>: setState 함수 타입

export interface FileInputProps extends FileGuideLineProps { file?: IFile | null; setFile: Dispatch<SetStateAction<File | null | undefined>>; downloadFileEndPoint?: string; // 파일 다운로드 API 주소 isReadOnly?: boolean; }

**.d.ts 파일

  • d.ts 파일은 TypeScript에서 타입 선언 파일로 사용됩니다. 이 파일은 주로 타입 정의를 확장하거나 커스터마이징하기 위해 사용됩니다.

  • d.ts 파일은 TypeScript 컴파일러에게 특정 모듈이나 라이브러리의 타입 정보를 제공하여, 코드 작성 시 타입 안전성을 높이고, 코드 자동 완성 기능을 개선하는 데 도움을 줍니다.

3.11 package.json

package.json은 Node.js 프로젝트의 메타데이터와 종속성(dependencies)을 관리하는 데 사용됩니다.

scripts

  • 로컬 환경에서 개발서버를 시작할 때

npm run local
  • 개발 환경에서 개발 서버를 시작할 때

npm run dev
  • 프로젝트를 빌드합니다. Next.js 애플리케이션을 프로덕션 환경에 배포할 수 있도록 최적화된 정적 파일로 빌드합니다.

npm run build
  • 빌드된 프로젝트를 시작합니다. next build 명령어로 빌드된 파일을 사용하여 프로덕션 서버를 시작합니다.

npm run start

dependencies, devDependencies

  • dependencies는 애플리케이션이 실행될 때 필요한 패키지를 정의합니다

  • devDependencies는 개발 환경에서만 필요한 패키지를 정의합니다.

  • 패키지 추가

npm i package-name
  • 패키지 삭제

npm rm package-name

package-lock.json

  • 생성된 node modules 폴더의 정보를 담고있는 파일

  • npm을 사용해서 node_modules 트리나 package.json 파일을 수정하게 되면 자동으로 생성/수정

  • package-lock.json 파일이 생성되는 시점의 의존성 트리에 대한 정확한 정보를 포함

3.12 Layout

  • 웹 화면 레이아웃 구성요소에 대한 설명입니다

GNB (Global Navigation Bar)

GNB는 웹 사이트의 전역 탐색 바를 의미합니다. 일반적으로 페이지 상단에 위치하며, 사이트의 주요 섹션으로 이동할 수 있는 링크를 포함합니다. GNB는 사용자가 사이트 내에서 쉽게 탐색할 수 있도록 도와줍니다.

주요 기능:

  • 사이트의 주요 섹션으로의 빠른 이동

  • 로고 및 브랜드 아이덴티티 표시

  • 사용자 로그인/로그아웃, 프로필 접근 등의 사용자 계정 관련 기능

LNB (Left Navigation Bar)

LNB는 왼쪽 탐색 바를 의미합니다. 페이지의 왼쪽에 위치하며, 현재 섹션 내에서의 세부 항목으로 이동할 수 있는 링크를 포함합니다. LNB는 사용자가 특정 섹션 내에서 세부 항목을 쉽게 탐색할 수 있도록 도와줍니다.

주요 기능:

  • 현재 섹션 내의 세부 항목으로의 빠른 이동

  • 섹션 내의 계층 구조 표시

  • 사용자가 현재 위치를 쉽게 파악할 수 있도록 도움

Header는 페이지의 상단에 위치한 영역으로, GNB와 함께 사이트의 주요 정보를 표시합니다. 일반적으로 사이트의 로고, 검색 바, 사용자 계정 정보 등을 포함합니다.

주요 기능:

  • 사이트의 로고 및 브랜드 아이덴티티 표시

  • 검색 기능 제공

  • 사용자 계정 정보 및 알림 표시

FNB는 페이지의 하단에 위치한 탐색 바를 의미합니다. 사이트의 주요 섹션으로 이동할 수 있는 링크와 추가적인 정보(예: 저작권 정보, 연락처 정보 등)를 포함합니다.

주요 기능:

  • 사이트의 주요 섹션으로의 빠른 이동

  • 저작권 정보 및 법적 고지 사항 표시

  • 연락처 정보 및 소셜 미디어 링크 제공

Page Header는 특정 페이지의 상단에 위치한 영역으로, 해당 페이지의 제목 및 주요 정보를 표시합니다. 사용자가 현재 어떤 페이지에 있는지 명확하게 인식할 수 있도록 도와줍니다.

주요 기능:

  • 페이지 제목 및 주요 정보 표시

  • 페이지의 목적 및 내용을 간략히 설명

Breadcrumb는 사용자가 현재 위치한 페이지의 경로를 표시하는 탐색 도구입니다. 일반적으로 페이지 상단에 위치하며, 사용자가 사이트 내에서 이동한 경로를 시각적으로 표시합니다.

주요 기능:

  • 사용자가 현재 위치한 페이지의 경로 표시

  • 사용자가 이전 페이지로 쉽게 이동할 수 있도록 도움

  • 사이트의 계층 구조를 시각적으로 제공

3.13 UI : L-UI

Introduction

캐모마일의 UI 컴포넌트 라이브러리는 L-UI를 사용합니다. L-UI는 Lotte Innovate 디자인 가이드를 적용한 전사 표준 UI 라이브러리입니다.

주요 특징

  • 웹 표준 및 접근성 준수

    • Headless UI Library인 radix-ui를 사용하여 웹 접근성을 보장합니다.

    • 정부 UI/UX 가이드라인을 준수하여 개발되었습니다.

  • 다양한 컴포넌트

    • 62가지 이상의 UI 컴포넌트와 6가지의 차트를 제공합니다.

  • 일관된 디자인

    • 모든 컴포넌트와 차트에 공통 Props (Color, Radius, Scaling, Weight, Appearance, Size)를 적용하여 디자인의 통일성을 유지합니다.

    • 다크모드에서도 표준화된 색상을 적용하여 모든 컴포넌트에 일관되게 적용합니다.

  • 스토리북 통합

  • npm 패키지 배포

Installation

npm i @lotte-innovate/lui

또는

yarn add @lotte-innovate/lui

Usage

프로젝트 App 진입 파일에 다음 줄을 추가해줍니다.

import '@lotte-innovate/lui/dist/globals.css';
import React from 'react'; import { Button } from '@lotte-innovate/lui'; import { MagnifyingGlassIcon } from '@radix-ui/react-icons'; const App = () => { return ( <div> <Button>Click Me</Button> <Avatar appearance="soft" fallback="A" /> <IconButton aria-label="iconButton" color="violet" radius="full" size="x-small"> <MagnifyingGlassIcon /> </IconButton> </div> ); };

Theme

각 컴포넌트들은 공통 Props를 개별적으로 적용할 수도 있고, 테마를 통해 컴포넌트의 스타일을 한번에 변경할 수도 있습니다.

L-UI는 테마 컨텍스트를 제공하여 Color, Radius, Scaling, Weight 를 모든 컴포넌트에 동일한 스타일을 지정할 수 있습니다.

ThemeProvider를 프로젝트의 가장 가까운 layout.tsx 파일에 추가해야 합니다.

이렇게 하면 ThemeProvider가 해당 레이아웃 내의 모든 컴포넌트에 테마 상태를 제공할 수 있습니다.

import React from 'react'; import { LuiThemeProvider } from '@lotte-innovate/lui'; const Layout = ({ children }: { children: React.ReactNode }) => { return <LuiThemeProvider>{children}</LuiThemeProvider>; }; export default Layout;

테마를 변경하려면 useTheme훅을 사용합니다.

useTheme훅은 현재 테마와 테마를 업데이트할 수 있는 함수를 제공합니다.

import { Button, useTheme } from '@lotte-innovate/lui'; const ThemeChanger = () => { const { theme, updateTheme } = useTheme(); // 초기 값 설정 시 theme.themeRadius = 'none'; theme.themeColor = 'olive'; theme.themeScaling = '90%'; theme.themeWeight = 'regular'; // 테마 변경 시 useEffect(() => { updateTheme({ themeRadius: 'full', themeColor: 'amber', themeScaling: '110%' }); }, []); return ( <div> <Button>버튼</Button> </div> ); };

테마가 적용되었지만 일부 컴포넌트는 스타일을 다르게 하고 싶은 경우, 각 컴포넌트에 직접 Props를 전달하여 스타일을 재정의할 수 있습니다.

import { useTheme, Button } from '@lotte-innovate/lui'; const ThemeChanger = () => { const { theme } = useTheme(); theme.themeRadius: 'full'; theme.themeColor: 'blue'; return ( <div> <Button>테마 적용 버튼</Button> <Button radius="none" color="crimson">테마 미적용 버튼</Button> </div> ); };

3.14 Style : TailwindCSS

Tailwind CSS 컨벤션 및 가이드

Tailwind CSS는 유틸리티 퍼스트 CSS 프레임워크로, 빠르고 쉽게 스타일링을 할 수 있도록 도와줍니다. 이 가이드는 tailwind.config.js 파일을 바탕으로 작성되었습니다.

className 예시

레이아웃

  • Flexbox: flex, flex-row, flex-col, justify-center, items-center

  • Grid: grid, grid-cols-1, grid-cols-2, gap-4

  • Spacing: p-4, m-4, mt-4, mb-4, ml-4, mr-4

  • Spacing: p-4, m-4, mt-4, mb-4, ml-4, mr-4, px-4, py-4, mx-4, my-4

배경 및 테두리

  • Background Color: bg-white, bg-gray-100, bg-blue-500

  • Border: border, border-2, border-gray-300, rounded, rounded-full

텍스트

  • Typography: text-sm, text-lg, text-xl, text-2xl, font-bold, font-semibold

  • Text Color: text-gray-700, text-blue-500, text-white

  • Text Alignment: text-left, text-center, text-right

크기

  • Width: w-1/2, w-full, w-screen

  • Height: h-1/2, h-full, h-screen

반응형 디자인

  • Responsive Prefixes: sm:, md:, lg:, xl:

    • 예: sm:text-sm, md:text-lg, lg:text-xl, xl:text-2xl

커스텀 값

Tailwind CSS에서는 p-[value], m-[value]와 같은 형태로 커스텀 값을 사용할 수 있습니다. 이 방법을 사용하면 Tailwind의 기본 스케일 외의 값을 사용할 수 있습니다.

<div className="m-[5px] bg-blue-500 p-[10px] text-white"> This div has custom padding and margin. </div>

Global CSS 파일에 정의

Tailwind CSS를 사용하는 프로젝트에서 global.css 파일을 사용하여 전역 스타일을 정의할 수 있습니다. 예를 들어, 기본적인 HTML 요소의 스타일을 정의할 수 있습니다.

@tailwind base; @tailwind components; @tailwind utilities; body { @apply bg-gray-100 text-gray-900; } h1, h2, h3, h4, h5, h6 { @apply font-bold; } a { @apply text-blue-500 hover:underline; }

Tailwind Config 파일에 커스텀 색상 정의

Tailwind CSS를 사용하는 프로젝트에서 global.css 파일을 사용하여 전역 스타일을 정의할 수 있습니다. 예를 들어, 기본적인 HTML 요소의 스타일을 정의할 수 있습니다.

module.exports = { theme: { extend: { colors: { primary: { light: '#6d28d9', DEFAULT: '#5b21b6', dark: '#4c1d95', }, secondary: { light: '#fbbf24', DEFAULT: '#f59e0b', dark: '#d97706', }, }, }, }, variants: {}, plugins: [], };

이렇게 정의한 커스텀 색상은 Tailwind CSS 클래스에서 사용할 수 있습니다.

<div className="bg-primary p-4 text-white">This is a primary background with white text.</div> <div className="bg-secondary p-4 text-white">This is a secondary background with white text.</div>

3.15 Form : React Hook Form

react-hook-form 컨벤션 및 가이드

Introduction

react-hook-form은 React의 훅을 사용하여 폼 상태와 유효성 검사를 관리하는 라이브러리입니다. 이 라이브러리는 간단하고 직관적인 API를 제공하여 폼을 쉽게 관리할 수 있게 해줍니다.

사용 이유

  • 성능 최적화: react-hook-form은 폼 상태를 최소한으로 리렌더링하여 성능을 최적화합니다.

  • 간단한 API: 직관적이고 사용하기 쉬운 API를 제공합니다.

  • 유효성 검사: 다양한 유효성 검사 방법을 지원합니다.

  • 작은 번들 크기: 가벼운 라이브러리로, 번들 크기를 최소화합니다.

기본 문법

react-hook-form을 사용하여 폼을 생성하는 기본적인 방법은 다음과 같습니다:

import React from 'react'; import { useForm } from 'react-hook-form'; const MyForm = () => { const { register, handleSubmit, formState: { errors }, } = useForm(); const onSubmit = (data) => { console.log(data); }; return ( <form onSubmit={handleSubmit(onSubmit)}> <input {...register('firstName', { required: true })} /> {errors.firstName && <span>This field is required</span>} <input type="submit" /> </form> ); }; export default MyForm;

이 프로젝트에서 react-hook-form을 사용한 컴포넌트

프로젝트에서 react-hook-form을 사용하여 만든 컴포넌트는 다음과 같습니다:

  • <FormItemTextField />

  • <FormItemSelect />

  • <FormItemRadio />

  • <FormItemFile />

  • <FormItemCheckbox />

  • <SearchForm />

각 컴포넌트들은 react-hook-form의 기본 Props인 field, required 등을 전달 받아 폼 형식으로 구현됩니다.

폼 구현 예제

위의 컴포넌트들을 사용하여 폼을 구현하는 방법은 다음과 같습니다:

import React, { useState } from 'react'; import { useForm, FormProvider, SubmitHandler, SubmitErrorHandler } from 'react-hook-form'; import FormItemTextField from './FormItemTextField'; import FormItemSelect from './FormItemSelect'; import FormItemRadio from './FormItemRadio'; import FormItemFile from './FormItemFile'; import FormItemCheckbox from './FormItemCheckbox'; interface FormValues { menuId: string; menuLvl: string; useYn: string; menuHelpUri: FileList; agreement: boolean; } const MyForm = () => { const methods = useForm<FormValues>(); const [menuHelpfile, setMenuHelpfile] = useState<File | null>(null); const menuHelpEndpoint = '/download/help'; const onSubmit: SubmitHandler<FormValues> = (data) => { console.log('Form Data:', data); }; const onError: SubmitErrorHandler<FormValues> = (errors) => { console.log('Form Errors:', errors); }; return ( <FormProvider {...methods}> <form onSubmit={methods.handleSubmit(onSubmit, onError)}> <FormItemTextField labelName="메뉴ID" field="menuId" isReadOnly /> <FormItemSelect labelName="메뉴 레벨" field="menuLvl" optionList={commonCodeMenuLevel} required isReadOnly={false} /> <FormItemRadio labelName="사용여부" field="useYn" optionList={commonCodeUse} required isReadOnly={false} /> <FormItemFile labelName="도움말 파일" field="menuHelpUri" file={menuHelpfile} setFile={setMenuHelpfile} downloadFileEndPoint={menuHelpEndpoint} acceptedFileTypes=".pdf" maxFileSize={[1024, 'MB']} /> <FormItemCheckbox labelName="동의" field="agreement" required /> <button type="submit">제출</button> </form> </FormProvider> ); }; export default MyForm;

에러 핸들링과 제출 방법

폼 제출 시 에러 핸들링과 제출 방법은 handleSubmit 함수를 사용하여 처리할 수 있습니다. handleSubmit 함수는 두 개의 콜백 함수를 인수로 받습니다: 하나는 제출 성공 시 호출되는 함수이고, 다른 하나는 에러 발생 시 호출되는 함수입니다.

<form onSubmit={methods.handleSubmit(onSubmit, onError)}>{/* 폼 필드들 */}</form>

onSubmit 함수

onSubmit 함수는 폼 데이터가 유효할 때 호출됩니다. 이 함수에서 폼 데이터를 처리할 수 있습니다.

const onSubmit: SubmitHandler<FormValues> = (data) => { console.log('Form Data:', data); };

onError 함수

onError 함수는 폼 데이터가 유효하지 않을 때 호출됩니다. 이 함수에서 에러를 처리할 수 있습니다.

const onError: SubmitErrorHandler<FormValues> = (errors) => { console.log('Form Errors:', errors); };

폼 상태 관리

react-hook-form은 폼 상태를 관리하기 위한 다양한 기능을 제공합니다.

isDirty

isDirty를 사용하여 폼이 수정되었는지 여부를 확인할 수 있습니다.

const { formState: { isDirty } } = methods; return ( <div> {isDirty && <span>폼이 수정되었습니다.</span>} </div> );

dirtyFields

dirtyFields는 수정된 필드들을 포함하는 객체입니다. 각 필드의 이름을 키로 사용하여 해당 필드가 수정되었는지 여부를 확인할 수 있습니다.

const { formState: { dirtyFields } } = methods; return ( <div> {dirtyFields.menuId && <span>메뉴ID 필드가 수정되었습니다.</span>} </div> );

isValid

isValid는 폼의 모든 필드가 유효한지 여부를 나타냅니다. 모든 필드가 유효할 때 true가 됩니다.

const { formState: { isValid } } = methods; return ( <div> {isValid ? <span>폼이 유효합니다.</span> : <span>폼이 유효하지 않습니다.</span>} </div> );

isSubmitting

isSubmitting은 폼이 제출 중인지 여부를 나타냅니다. 폼이 제출되는 동안 true가 됩니다.

const { formState: { isSubmitting } } = methods; return ( <button type="submit" disabled={isSubmitting}> {isSubmitting ? '제출 중...' : '제출'} </button> );

isSubmitted

isSubmitted는 폼이 한 번이라도 제출되었는지 여부를 나타냅니다. 폼이 제출된 후 true가 됩니다.

const { formState: { isSubmitted } } = methods; return ( <div> {isSubmitted && <span>폼이 제출되었습니다.</span>} </div> );

errors

errors는 폼 필드의 유효성 검사 오류를 포함하는 객체입니다. 각 필드의 이름을 키로 사용하여 해당 필드의 오류 메시지를 확인할 수 있습니다.

const { formState: { errors } } = methods; return ( <div> {errors.menuId && <span>{errors.menuId.message}</span>} </div> );

touchedFields

touchedFields는 사용자가 상호작용한 필드들을 포함하는 객체입니다. 각 필드의 이름을 키로 사용하여 해당 필드가 터치되었는지 여부를 확인할 수 있습니다.

const { formState: { touchedFields } } = methods; return ( <div> {touchedFields.menuId && <span>메뉴ID 필드가 터치되었습니다.</span>} </div> );

그 외 주요 기능들

react-hook-form은 폼을 관리하고 유효성 검사를 수행하기 위한 다양한 훅과 컴포넌트를 제공합니다. 여기에는 Controller, useForm, useFormContext, watch 등이 포함됩니다.

useForm

useForm 훅은 폼을 관리하기 위한 기본 훅입니다. 이 훅은 폼 상태와 유효성 검사를 관리하는 데 필요한 메서드와 객체를 반환합니다.

import { useForm } from 'react-hook-form'; const MyForm = () => { const { register, handleSubmit, formState: { errors } } = useForm(); const onSubmit = data => { console.log(data); }; return ( <form onSubmit={handleSubmit(onSubmit)}> <input {...register("firstName", { required: true })} /> {errors.firstName && <span>This field is required</span>} <input type="submit" /> </form> ); };

Controller

Controller 컴포넌트는 비제어 컴포넌트를 react-hook-form과 함께 사용할 수 있도록 도와줍니다. Controllerrender prop을 사용하여 폼 필드를 렌더링하고, 폼 상태와 유효성 검사를 관리합니다.

import { useForm, Controller } from 'react-hook-form'; import Select from 'react-select'; const MyForm = () => { const { control, handleSubmit } = useForm(); const onSubmit = data => { console.log(data); }; return ( <form onSubmit={handleSubmit(onSubmit)}> <Controller name="selectOption" control={control} render={({ field }) => <Select {...field} options={[{ value: 'A', label: 'Option A' }, { value: 'B', label: 'Option B' }]} />} /> <input type="submit" /> </form> ); };

useFormContext

useFormContext 훅은 폼 컨텍스트를 사용하여 중첩된 컴포넌트에서 폼 상태와 메서드에 접근할 수 있도록 합니다. 이 훅은 FormProvider 컴포넌트와 함께 사용됩니다.

import { useForm, FormProvider, useFormContext } from 'react-hook-form'; const NestedInput = () => { const { register } = useFormContext(); return <input {...register("nestedInput")} />; }; const MyForm = () => { const methods = useForm(); const onSubmit = data => { console.log(data); }; return ( <FormProvider {...methods}> <form onSubmit={methods.handleSubmit(onSubmit)}> <NestedInput /> <input type="submit" /> </form> </FormProvider> ); };

watch

watch 훅은 폼 필드의 값을 관찰하고, 값이 변경될 때마다 업데이트를 받을 수 있도록 합니다. 특정 필드나 모든 필드의 값을 관찰할 수 있습니다.

import { useForm } from 'react-hook-form'; const MyForm = () => { const { register, handleSubmit, watch } = useForm(); const watchAllFields = watch(); // 모든 필드 관찰 const watchSpecificField = watch("firstName"); // 특정 필드 관찰 const onSubmit = data => { console.log(data); }; return ( <form onSubmit={handleSubmit(onSubmit)}> <input {...register("firstName")} /> <input {...register("lastName")} /> <input type="submit" /> <div> <p>First Name: {watchSpecificField}</p> <p>All Fields: {JSON.stringify(watchAllFields)}</p> </div> </form> ); };

3.16 Date : Day.js

date 라이브러리 - dayjs

dayjs는 JavaScript 날짜 라이브러리로, 날짜와 시간을 쉽게 다룰 수 있도록 도와줍니다. moment.js와 유사한 API를 제공하지만, 더 가볍고 빠른 성능을 지원합니다.

https://day.js.org/

날짜 표시

const date = dayjs('2024-12-11T11:56:00+09:00'); console.log(date.year()); // 2024 (연도) console.log(date.month()); // 12 (월) console.log(date.date()); // 11 (일) console.log(date.hour()); // 11 (시) console.log(date.minute()); // 56 (분) console.log(date.second()); // 0 (초) console.log(date.format('YYYY.MM.DD (HH:mm)')); // '2024.12.11 (11:56)'

날짜 변경

// set: 날짜를 변경할 수 있습니다. let date = dayjs('2024-12-11T11:56:00+09:00'); date = date.set('month', 12); // 12월로 달 변경 console.log(date.format('YYYY년 MM월 DD일')); // '2024년 12월 11일'

날짜 더하기

// add: 날짜를 더해줄 수 있습니다. date = dayjs('2024-12-11T11:56:00+09:00'); date = date.add(12, 'day'); // 12일 더하기 console.log(date.format('YYYY년 MM월 DD일')); // '2024년 12월 23일'

날짜 빼기

// subtract: 날짜를 빼줄 수 있습니다. date = dayjs('2024-12-11T11:56:00+09:00'); date = date.subtract(6, 'month'); // 6개월 빼기 console.log(date.format('YYYY년 MM월 DD일')); // '2024년 06월 11일'

3.17 Grid : TanStack Table

Grid - Tanstack Table Guide

Introduction

L-UI 의 Table 컴포넌트는 Tanstack Table을 커스텀하여 사용합니다.

TanStack Table은 React 애플리케이션에서 유연한 테이블을 구성할 수 있도록 도와주는 라이브러리입니다. 이전에는 react-table로 알려져 있었으며, 다양한 기능과 높은 성능을 제공하여 복잡한 테이블을 쉽게 구현할 수 있습니다.

주요 기능

  • 정렬: 컬럼별 정렬 기능 제공

  • 페이징: 데이터 페이징 처리

  • 필터링: 데이터 필터링 기능

  • 그룹화: 데이터 그룹화 기능

  • 확장: 행 확장 기능 제공

  • 선택: 행 선택 기능 제공

TanStack Table 선정 이유

프로젝트에서 TanStack Table을 사용하는 이유는 다음과 같습니다:

  • 유연성: 다양한 기능을 손쉽게 커스터마이징할 수 있습니다.

  • 성능: 대용량 데이터 처리에 최적화되어 있습니다.

  • 확장성: 필요한 기능을 쉽게 확장할 수 있습니다.

  • 커뮤니티 지원: 활발한 커뮤니티와 풍부한 문서 지원을 받습니다.

Usage

useReactTable을 커스터마이징한 useGrid훅과 L-UI에서 Table, @tanstack/react-table을 활용하여 만든 그리드 컴포넌트 <Grid />을 함께 사용합니다.

const ExampleComponent = () => { const [gridData, setGridData] = useState([ // 데이터 예시 { id: 1, name: 'John', age: 28 }, { id: 2, name: 'Jane', age: 32, subRows: [{ id: 3, name: 'Kim', age: 20 }] }, ]); const columns = useMemo( () => [ { id: 'id', header: 'ID', }, { id: 'name', header: 'Name', }, { id: 'age', header: 'Age', }, ], [] ); const [columnVisibility, setColumnVisibility] = useState({ id: false, // id 컬럼은 안보이게 처리 }); const onRowClick = (data) => { // 행 클릭 시 로직을 작성합니다. }; const table = useGrid({ data: gridData, columns: columns, enableRowSelection: true, // 행 선택 기능 활성화 enableSorting: true, // 컬럼 정렬 기능 활성화 enableRowPagination: true, // 행 페이징 기능 활성화 enableExpanding: true, // 행 확장 기능 활성화 initialState: { columnVisibility, // 초기 컬럼 가시성 설정 }, onColumnVisibilityChange: setColumnVisibility, // 컬럼 가시성 변경 핸들러 onRowClick: onRowClick, // 행 클릭 이벤트 핸들러 disableInputPagination: true, // 입력 페이징 비활성화 }); return ( <div className="h-[474px]"> <Grid table={table} /> </div> ); };

Tanstack Table 기타 속성

  1. Column Visibility 설정

  • 그리드 컬럼에 대한 숨김 여부는 useGrid()initialState onColumnVisibilityChange 속성을 통해 처리할 수 있습니다.

// 숨길 컬럼 id에 false 값을 넣어줍니다. const [columnVisibility, setColumnVisibility] = useState({ menuId: false, menuLvl: false, upperMenuId: false, }); const table = useGrid({ data: gridData, columns: gridColumnList, // 컬럼 초기 상태를 설정해줍니다. initialState: { columnVisibility, }, // 컬럼 visibility 상태를 설정해줍니다. onColumnVisibilityChange: setColumnVisibility, });

TanStack Table 사용 시 주의점 ‼️

  1. 데이터 상태관리

  • 리액트 개발 환경에서 Grid에 전달되는 datacolumnsstable한 상태로 제공되어야 합니다. 그렇지 않은 경우 무한 루프를 발생시킬 수 있습니다.

  • datacolumnsReact.useMemo(), React.useState()을 사용해야 합니다.

  • 해당 내용은 그리드 데이터 참조 가이드에서 자세하게 볼 수 있습니다.

const fallbackData = [] const GoodExampleComponent = () => { //✅ GOOD const columns = useMemo(() => [ // ... ], []); //✅ GOOD const [data, setData] = useState(() => [ // ... ]); const table = useGrid({ columns, data ?? fallbackData, // fallbackData를 컴포넌트 외부에 두어 stable한 상태를 만들어주는 것도 좋은 방법입니다 }); return <Grid table={table} /> }
const fallbackData = [] const BadExampleComponent = () => { //😵 BAD: `columns`가 모든 렌더링에서 새로운 배열로 재정의되기 때문에 렌더링을 무한 루프로 반복하게 됩니다 const columns = [ // ... ]; //😵 BAD: `data`가 모든 렌더링에서 새로운 배열로 재정의되기 때문에 렌더링을 무한 루프로 반복하게 됩니다 const data = [ // ... ]; //❌ `columns`, `data` 가 `useGrid`와 동일한 스코프에 위치하면서 `stable`하지 않은 변수로 선언될 경우 무한 루프가 발생합니다 const table = useGrid({ columns, data ?? [], //❌ fallback 배열이 렌더링마다 매번 생성됩니다 }); return <Grid table={table} /> }
  1. columns의 동적 변경

  • 특정 상황에서는 columns를 동적으로 변경해야 할 필요가 있을 수 있습니다.

  • 이 경우 useState를 사용하는 것이 적절합니다. 예를 들어, 사용자가 컬럼을 동적으로 추가하거나 제거할 수 있는 기능을 구현하려는 경우 useState를 사용하는 것이 더 적합합니다.

이 외에도 Tanstack Table에서 제공하는 다양한 함수와 옵션은 Tanstack Table 공식 문서를 참고하세요.

L-UI상에서 테마 적용과 간단한 예시는 L-UI Grid 스토리북 가이드를 참고하세요.

3.18 Editor : TinyMCE

Editor - Tinymce Guide

Introduction

L-UIEditor 컴포넌트는 Tinymce을 커스텀하여 사용합니다.

TinyMCE는 웹 애플리케이션에서 사용할 수 있는 강력한 WYSIWYG(What You See Is What You Get) HTML 에디터입니다. 사용자가 텍스트를 입력하고 포맷팅할 수 있는 인터페이스를 제공하며, 다양한 기능과 플러그인을 통해 확장할 수 있습니다.

TinyMCE는 오픈 소스이며, 현재는 다양한 상용 플랜도 제공하고 있습니다. L-UIEditor는 무료 플랜을 기반으로 커스텀되어 있습니다.

주요 기능

  • 텍스트 포맷팅: 굵게, 기울임, 밑줄, 글자 색상 등 다양한 텍스트 포맷팅 기능 제공

  • 리스트: 순서 있는 리스트와 순서 없는 리스트 생성

  • 링크: 하이퍼링크 삽입 및 편집

  • 이미지: 이미지 삽입 및 편집

  • : 표 삽입 및 편집

  • 플러그인: 다양한 플러그인을 통해 기능 확장 가능

  • 테마: 다양한 테마와 스킨을 통해 에디터의 외관 커스터마이징 가

TinyMCE 선정 이유

프로젝트에서 TinyMCE를 사용하는 이유는 다음과 같습니다:

  • 사용자 친화적: 직관적인 사용자 인터페이스를 제공하여 사용자가 쉽게 사용할 수 있습니다.

  • 풍부한 기능: 기본적인 텍스트 편집 기능 외에도 다양한 고급 기능을 제공합니다.

  • 확장성: 플러그인 시스템을 통해 기능을 쉽게 확장할 수 있습니다.

  • 커스터마이징: 테마와 설정을 통해 에디터의 외관과 동작을 커스터마이징할 수 있습니다.

  • 커뮤니티 지원: 활발한 커뮤니티와 풍부한 문서 지원을 받습니다.

Usage

기본적인 Editor 컴포넌트 사용방법 입니다.

import { Editor } from '@lotte-innovate/lui'; const ExampleComponent = () => { return <Editor />; };

language props로 에디터의 언어를 변경합니다.

한국어, 영어, 중국어, 일본어, 베트남어, 이탈리아어를 지원하며 기본 값은 영어 입니다.

import { Editor } from '@lotte-innovate/lui'; const ExampleComponent = () => { return ( <> <Editor language="ko_KR" /> // 한국어 <Editor language="en" /> // 영어 <Editor language="zh_CN" /> // 중국어 <Editor language="ja" /> // 일본어 <Editor language="vi" /> // 베트남어 <Editor language="it" /> // 이탈리아어 </> ); };

필요한 기능만 선택적으로 로드할 수 있습니다.

import { Editor } from '@lotte-innovate/lui'; const ExampleComponent = () => { return ( <Editor className="w-[700px]" init={{ menubar: '', toolbar: 'blocks | bold italic forecolor strikethrough | hr blockquote | bullist numlist outdent indent | table image link | inlinecode codeformat codesample markdown', }} /> ); };

이 외에도 TinyMCE에서 제공하는 다양한 함수와 옵션은 TinyMCE 공식 문서를 참고하세요.

L-UI상에서 테마 적용과 간단한 예시는 L-UI Editor 스토리북 가이드를 참고하세요.

3.19 Menus

  • 메뉴 렌더링은 전체 레이아웃/라우팅에 밀접하게 연관되어 있습니다.

  • Gnb, Lnb, MdiTabs, PageHeaderBreadcrumb, GnbMenu, LnbLogo 컴포넌트에서 사용되며, 여러 컴포넌트에서 광범위하게 상태를 참조하므로, menu store에서 전역적으로 관리합니다.

  • buildMenuTree: 주어진 메뉴 아이템 리스트를 트리 구조로 변환합니다.

  • selectLogoAndGnbMenus: 주어진 메뉴 트리에서 menuName이 환경변수 NEXT_PUBLIC_ROOT_MENU_ID에서 지정한 메뉴를 logoMenu로 지정하고, 나머지 메뉴들을 gnbMenus로 지정하여 반환합니다.

  • filterMenuTree: 검색어에 따라 메뉴를 필터링하는 함수입니다. 해당하는 메뉴와 연관된 메뉴는 남기고, 전혀 관계없는 메뉴는 필터링합니다.

  • getDefaultMenu: 주어진 메뉴 아이템 배열에서 기본 메뉴를 찾는 함수입니다.

  • findMenuByPathname: 주어진 메뉴 아이템 배열에서 주어진 경로와 일치하는 메뉴를 찾는 함수입니다.

  • findCurrentMenuAndParents: 주어진 메뉴 아이템 배열에서 현재 메뉴와 상위 부모 메뉴들을 찾는 함수입니다.

  • findMenuByUri: 주어진 경로(pathname)와 일치하는 메뉴 항목을 찾는 함수입니다.

  • createSvgIcon: 메뉴 이름(title)을 받아 해당 이름의 첫 글자를 포함한 SVG 아이콘을 생성하는 함수입니다.

  • Gnb 컴포넌트에서 leftMenuList queryKey로 호출하여 fetch합니다.

  • 메뉴리스트는 변하지 않고 계속 사용할수 있으므로, gcTime staleTime 무한대로 주어 캐싱후 다시 fetch하지 않도록합니다.

const { data: menuList, error: menuListError, isLoading, } = useGetQuery<IQueryResult>({ queryKey: ['leftMenuList'], endpoint: '/chmm/menu/left', gcTime: Infinity, staleTime: Infinity, });
  • Gnb 컴포넌트에서 menuList 데이터가 로드되면, buildMenuTree로 트리형태로 만들고, NEXT_PUBLIC_ROOT_MENU_ID 상에 정의돈 메뉴는 LogoMenu (로고 누르면 나오는 루트메뉴), 나머지 메뉴들은 GnbMenus로 할당해줍니다.

// 데이터가 성공적으로 로드되었을 때 실행할 로직 useEffect(() => { if (menuList) { // buildMenuTree를 이용해서 data를 tree 구조로 만들어줌 const menuTree = buildMenuTree(menuList.data); setTreeMenus(menuTree); setOriginMenus(menuList.data); // logo와 gnb영역에 렌더링할 메뉴 데이터 선정 const { logoMenu, gnbMenus } = selectLogoAndGnbMenus(menuTree); setLogoMenu(logoMenu); setGnbMenus(gnbMenus); } }, [menuList]);

메뉴 상태를 도식화 하면 아래와 같습니다.

leftMenu ├── LogoMenu │ ├── LnbMenuItem1 │ ├── LnbMenuItem2 │ └── ... └── GnbMenus ├── GnbMenu1 │ ├── LnbMenuItem1 │ ├── LnbMenuItem2 │ └── ... ├── GnbMenu2 │ ├── LnbMenuItem1 │ ├── LnbMenuItem2 │ └── ... └── ...

Rendering Lnb

  • Gnb메뉴에서 선택한 메뉴의 하위 항목들은 lnbMenus 상태로 저장되며, Lnb 컴포넌트에서 이 lnbMenus 데이터가 로드되면, 메뉴명 검색 로직을 거쳐 filteredMenus가 최종 Lnb에 렌더링 됩니다.

  • L-UI에서 제공하는 Sidebar 컴포넌트를 활용하여 렌더링됩니다.

  • 기존 Chamomile Admin Vue에서 사용하던 menu 경로가 Pascal Case인 관계로 pascalToKebabCase 함수를 사용하여 pathname을 변경시켜줍니다.

  • 메뉴의 하위 메뉴가 있는 경우 없는경우를 구분하여 각각 <Sidebar.Item /> <Sidebar.SubMenu />으로 렌더링합니다.

  • 현재 pathname과 비교하여 일치하거나 상위메뉴인 경우에는 active 처리해줍니다.

  • 탭 최대 20개를 고려하여, 20개를 초과하면 페이지 이동을 하지않고, 20개 이하일땐 router.push()로 화면 이동합니다.

const handleItemClick = (menuUri: string) => { if (pages.length < 20) { router.push(`/${params.lng}${pascalToKebabCase(menuUri)}`); // 메뉴 클릭 시 해당 경로로 이동 } else { if (pages.find((page) => page.href === pascalToKebabCase(menuUri))) { // 20개 페이지 이상인 경우, 기존 탭이 존재하면 해당 탭으로 이동 router.push(`/${params.lng}${pascalToKebabCase(menuUri)}`); } else { showToast('오류', '탭이 20개를 초과하여 더이상 열 수 없습니다.'); } } };

MdiTabs

  • MDI Tabs에서도 MenuStore의 메뉴정보들을이용해서 TabItem을 만들어주고 렌더링합니다.

  • Tab Item들을 추가/삭제 할때에도 메뉴 정보가 필요합니다.

const originMenus = useMenuStore((state) => state.originMenus); const treeMenus = useMenuStore((state) => state.treeMenus); const defaultTabItem = { id: pascalToKebabCase(defaultMenu.menuUri), title: defaultMenu.menuName, href: pascalToKebabCase(defaultMenu.menuUri), menuCode: generateMenuCode(defaultMenu.menuId), };

PageHeaderBreadcrumb

  • L-UI에서 제공하는 Breadcrumb 컴포넌트를 활용하여 렌더링됩니다.

  • findCurrentMenuAndParents를 사용하여 현재 메뉴와 부모 메뉴를 찾고 브레드크럼 목록을 설정합니다.

  • 즐겨찾기 추가/삭제 로직이 포함되어 있습니다.

3.20 MdiTabs : React Activation

MDI는 Multiple Document Interface의 약자로, 하나의 애플리케이션 창 안에서 여러 개의 문서나 화면을 탭 형태로 열 수 있게 해주는 인터페이스를 의미합니다. react-activationreact-chrome-tabs를 사용하여 구현하였습니다.

  • react-activation: 페이지 전환 시 컴포넌트의 상태를 유지하기 위해 사용하였습니다.

  • react-chrome-tabs: 사용자에게 친숙한 UX를 제공하기 위해, 구글 Chrome과 유사한 Tab을 제공해주는 react-chrome-tabs를 사용하여 MDI Tab을 구현하였습니다.

react-activation

  • MDI를 구성하려면, 활성회된 메뉴 목록이 탭에 표시되어야하며, 화면 이동을 하더라도 작업을 하던 화면의 상태가 유지 되어야합니다.

  • <AliveScope />는 KeepAlive 컴포넌트들이 상태를 공유할 수 있는 범위를 정의합니다.

  • <KeepAlive />는 특정 컴포넌트의 상태를 유지하고 복원하는 역할을 합니다

<div className="flex-1"> <Gnb /> <MdiTabs /> {isMounted && ( <div className="bg-[#F3F3F7]"> <AliveScope> <KeepAlive key={pathnameWithoutLng}> <main className="p-5"> <div className="rounded-xl bg-white p-2">{children}</div> </main> </KeepAlive> </AliveScope> </div> )} </div>

mdi-tabs.tsx (react-chrome-tabs)

  • 드래그 앤 드롭: 탭을 드래그하여 순서를 변경할 수 있습니다.

  • 탭 닫기: 탭을 닫을 수 있는 기능을 제공합니다.

  • 탭 추가: 새로운 탭을 추가할 수 있습니다.

const Tabs = dynamic(() => import('@sinm/react-chrome-tabs').then((mod) => mod.Tabs), { ssr: false, }); <div className="ml-2 flex"> <nav className="flex cursor-pointer gap-1"> <div className="flex overflow-x-hidden" style={{ width: flag ? `calc(100vw - 13rem)` : `calc(100vw - 24rem)` }} > <Tabs className="flex-1" onTabClose={close} onTabReorder={reorder} onTabActive={active} tabs={tabs} /> </div> </nav> <div className="ml-auto flex" style={{ width: '4.5rem' }}> <Button onClick={closeAll} appearance="ghost"> <TrashIcon /> </Button> </div> </div>;

Page Stack

  • Page Stack은 store/page-stack.ts 에서 관리됩니다.

interface IPagesInfo { pages: ITabItem[]; currentPage: ITabItem | null; actions: { setCurrentPage: (tab: ITabItem) => void; pushStack: (tab: ITabItem) => void; popStack: (tab: ITabItem) => void; clearStack: (defaultTabItem: ITabItem) => void; setPages: (pages: ITabItem[]) => void; }; }

3.21 reCAPTCHA

캐모마일에서는 Google reCAPTCHA v2 를 사용하고 있습니다.

1. reCAPTCHA v2 개요

reCAPTCHA v2의 2가지 방식

  • Checkbox reCAPTCHA: 사용자가 "I'm not a robot" 체크박스를 클릭합니다. 이 방식은 사용자가 사람임을 확인하기 위해 추가적인 작업을 요구할 수 있습니다.

  • Invisible reCAPTCHA: 사용자가 특정 작업을 수행할 때(예: 폼 제출) 자동으로 트리거됩니다. 사용자는 추가적인 작업을 수행하지 않아도 됩니다.

2. reCAPTCHA v2 설정

  • Google reCAPTCHA 사이트 등록

    1. Google reCAPTCHA 관리자 콘솔 이동

    2. Google 계정 로그인

    3. "새 사이트 등록" 클릭

    4. 정보 입력

      • 라벨: 사이트 식별 명

      • reCAPTCHA 유형: "테스트(v2)"를 선택 -> "로봇이 아닙니다." 체크박스 선택

      • 도메인: reCAPTCHA를 사용할 도메인

    5. "제출" 버튼을 클릭

    6. 사이트 키와 비밀 키 생성

3. ReCaptcha 컴포넌트 사용 방법

  • .env 설정

    #ReCaptcha NEXT_PUBLIC_RECAPTCHA_SITE_KEY="['2. reCAPTCHA v2 설정'에서 받은 사이트 키]"
  • 컴포넌트 JSX 설정

    • reCAPTCHA 결과를 onResponse 를 통해서 받아서 사용

<ReCaptcha onResponse={(value: boolean) => { // reCAPTCHA 결과 이후 로직 }} />

3.22 Copy & Cut

1. 기능 설명

  • 이 기능은 웹 페이지에서 텍스트를 복사하거나 잘라내는 것을 방지합니다. 사용자가 복사 또는 잘라내기 동작을 시도할 때, 기본 동작이 차단되고 경고 메시지가 표시됩니다.

2. 기능 활성화 방법

  1. 환경 변수 설정

    ## Copy & Cut 방지 유무 NEXT_PUBLIC_COPY_CUT=true
Last modified: 21 4월 2025