기존 에러관리 코드 

 

// 커스텀 에러 클래스들

import { ValidationMessages } from "@/types/authTypes";
import convertAuthSupabaseErrorToKorean from "./convertAuthSupabaseErrorToKorean";
import convertStorageSupabaseErrorToKorean from "./convertStorageSupabaseErrorToKorean";

export class ApiError extends Error {
  constructor(public status: number, message: string) {
    super(message);
    this.name = "ApiError";
  }
}

export class SupabaseAuthError extends Error {
  code: string;

  constructor(code: string, message: string) {
    super(message);
    this.code = convertAuthSupabaseErrorToKorean(code);
    this.name = "SupabaseAuthError";

    // 프로토타입 체인 유지를 위한 설정
    Object.setPrototypeOf(this, SupabaseAuthError.prototype);
  }

  toJSON() {
    return {
      name: this.name,
      message: this.message,
      code: this.code,
    };
  }
}

export class SupabaseStorageError extends Error {
  constructor(public code: string, message: string) {
    super(message);
    this.code = convertStorageSupabaseErrorToKorean(code);
    this.name = "SupabaseStorageError";
  }
}

export class ValidationError extends Error {
  constructor(public fieldErrors: ValidationMessages, public code?: string) {
    super(Object.values(fieldErrors)[0]);
    this.name = "ValidationError";
  }
}

export class PasswordError extends Error {
  constructor(message: string, public code?: string) {
    super(message);
    this.name = "PasswordError";
  }
}

 

에러를 효율적으로 관리하기 위해 class를 만들어 구현하려고 했는데
이 때문에 오히려 점점 복잡해 졌다

결정적으로 server에서 client로 넘길 때 class를 넘기는 것은 안되기 때문에 고치기로 결정

전달 가능한 데이터 타입:
✅ Plain objects ({})
✅ Arrays ([])
✅ Numbers
✅ Strings
✅ Booleans
✅ null
✅ undefined
❌ Class instances
❌ Functions
❌ Dates (직렬화 필요)
❌ Regular Expressions
❌ Maps/Sets

전달 가능한 데이터 타입은 이와 같다고 한다

 

바뀐 방법은 

import { AuthError, PostgrestError } from "@supabase/supabase-js";

interface ValidationError extends Error {
  name: "ValidationError";
  fieldErrors: Record<string, string[]>;
}

interface PasswordError extends Error {
  name: "PasswordError";
}

export function isValidationError(error: ApiError): error is ValidationError {
  return error.name === "ValidationError";
}

export function isAuthError(error: ApiError): error is AuthError {
  return error.name === "AuthError";
}

export function isPostgrestError(error: ApiError): error is PostgrestError {
  return error.name === "PostgrestError";
}

export function isPasswordError(error: ApiError): error is PasswordError {
  return error.name === "PasswordError";
}

export type ApiError =
  | ValidationError
  | AuthError
  | PostgrestError
  | PasswordError;

 

이렇게 타입스크립트로 관리하고 

하나의 ApiError로 묶어서 관리하기

또한 타입가드들도 생성하여 안정성을 더했다

 

이렇게 하면 기존의 했던 방법은 요청마다  클래스를 구분해서 넣어줘야 하지만
이 방법은 ApiError로 퉁 칠수 있어서 더욱 편해졌다 또한

import convertAuthSupabaseErrorToKorean from "@/error/convertAuthSupabaseErrorToKorean";
import convertStorageSupabaseErrorToKorean from "@/error/convertStorageSupabaseErrorToKorean";
import {
  ApiError,
  isAuthError,
  isPasswordError,
  isPostgrestError,
  isValidationError,
} from "@/types/error";

function formatApiErrorForUser(error: ApiError) {
  if (isValidationError(error)) {
    return {
      name: "ValidationError",
      message: "입력 값을 확인해주세요.",
      fieldErrors: error.fieldErrors,
    };
  } else if (isAuthError(error)) {
    console.log(error);
    return {
      name: "AuthError",
      message:
        convertAuthSupabaseErrorToKorean(error.code || "") || error.message,
    };
  } else if (isPostgrestError(error)) {
    return {
      name: "PostgrestError",
      message: convertStorageSupabaseErrorToKorean(error.code) || error.message,
    };
  } else if (isPasswordError(error)) {
    return {
      name: "PasswordError",
      message: error.message,
    };
  } else {
    return {
      name: "UnknownError",
      message: "예기치 않은 오류가 발생했습니다.",
    };
  }
}

export default formatApiErrorForUser;

통합되지 않은 에러 타입들로 인한 장애와

api에 나오는 메시지는 영어라는 문제를 해결하기 위해
handler를 사용해 불편한 사항들을 모두 해결하였다.

'개발 > NEXTJS' 카테고리의 다른 글

모달창에서 스크롤 방지  (0) 2025.02.10
error.tsx 사용  (0) 2025.02.10
사용자 avata image 변경했을 때  (0) 2025.01.31
좋은 블로그 글 모음  (1) 2025.01.27
nextjs에서 swr과 react query 비교  (0) 2025.01.27

Next.js의 병렬 라우팅과 일반 컴포넌트 방식 비교

1. 구현 방식 비교

일반적인 컴포넌트 방식:

// app/dashboard/page.tsx
import { Suspense } from 'react'

export default function Dashboard() {
  return (
    <div className="grid grid-cols-2 gap-4">
      <Suspense fallback={<div>Loading Dashboard...</div>}>
        <DashboardContent />
      </Suspense>

      <Suspense fallback={<div>Loading Analytics...</div>}>
        <AnalyticsContent />
      </Suspense>
    </div>
  )
}

async function DashboardContent() {
  const data = await fetchDashboardData()
  return <div>{/* 대시보드 내용 */}</div>
}

async function AnalyticsContent() {
  const data = await fetchAnalyticsData()
  return <div>{/* 분석 내용 */}</div>
}

병렬 라우팅 방식:

// app/@dashboard/page.tsx
export default async function Dashboard() {
  const data = await fetchDashboardData()
  return <div>{/* 대시보드 내용 */}</div>
}

// app/@analytics/page.tsx
export default async function Analytics() {
  const data = await fetchAnalyticsData()
  return <div>{/* 분석 내용 */}</div>
}

2. 주요 차이점

캐싱 메커니즘:

일반 컴포넌트 방식:

// 하나의 캐시 범위 내에서 동작
export default function Dashboard() {
  return (
    <div>
      <Suspense>
        <DashboardContent /> // 같은 캐시 범위 공유
        <AnalyticsContent /> // 같은 캐시 범위 공유
      </Suspense>
    </div>
  )
}

병렬 라우팅 방식:

// app/@dashboard/page.tsx - 독립적인 캐시 범위
export default async function Dashboard() {
  const data = await fetch('/api/dashboard', { next: { revalidate: 60 } })
  return <div>{/* ... */}</div>
}

// app/@analytics/page.tsx - 독립적인 캐시 범위
export default async function Analytics() {
  const data = await fetch('/api/analytics', { next: { revalidate: 3600 } })
  return <div>{/* ... */}</div>
}

주요 차이점

  1. 캐시 무효화 범위
// 일반 컴포넌트: 전체 페이지가 영향받음
revalidatePath('/dashboard')

// 병렬 라우팅: 개별 세그먼트만 영향받음  
revalidatePath('/dashboard/@analytics')
  1. 데이터 재사용
// 일반 컴포넌트
const data = await fetchData() // 한 번 fetch하면 페이지 내에서 재사용

// 병렬 라우팅
// @dashboard와 @analytics에서 같은 데이터를 fetch해도 
// 독립적으로 캐시되고 관리됨
  1. 메모리 사용
// 일반 컴포넌트  
const cache = new Map() // 하나의 캐시 공간

// 병렬 라우팅  
const dashboardCache = new Map() // 독립적인 캐시 공간  
const analyticsCache = new Map() // 독립적인 캐시 공간

3. 각 방식의 장단점

일반 컴포넌트 방식의 장점:

  • 더 단순한 파일 구조
  • 익숙한 컴포넌트 패턴
  • 컴포넌트 간 상태 공유가 더 쉬움
  • 더 유연한 레이아웃 조정 가능
  • 메모리 효율성이 좋음
  • 데이터가 자주 함께 업데이트되는 경우 적합

병렬 라우팅의 장점:

  • Next.js의 자동 코드 분할
  • 독립적인 에러 바운더리
  • 독립적인 로딩 상태
  • 섹션별 독립적인 캐싱 전략
  • 더 명확한 관심사 분리
  • 독립적인 데이터 갱신 주기가 필요할 때 적합

4. 선택 기준

일반 컴포넌트 방식이 적합한 경우:

  • 작은 규모의 프로젝트
  • 단순한 데이터 업데이트 패턴
  • 컴포넌트 간 긴밀한 상호작용이 필요한 경우
  • 메모리 효율성이 중요한 경우

병렬 라우팅이 적합한 경우:

  • 큰 규모의 프로젝트
  • 섹션별 독립적인 데이터 갱신이 필요한 경우
  • 섹션별 다른 에러 처리가 필요한 경우
  • 코드 분할이 중요한 경우

⚠ The "images.domains" configuration is deprecated. Please use "images.remotePatterns" configuration instead.
와 같은 에러가 발생하여 claude에 물어보니 
외부 Image 도메인 설정하는 방법이 바뀌었다고 한다

아래와 같이 해야한다

 

const nextConfig: NextConfig = {
  /* config options here */
  images: {
    domains: [
      "ui-avatars.com", // 기본 아바타 이미지
      "lh3.googleusercontent.com", // 실제 사용자 이미지 도메인
    ],
  },
};

이전

const nextConfig: NextConfig = {
  /* config options here */
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'ui-avatars.com',
        port: '',
        pathname: '/**',
      },
      {
        protocol: 'https',
        hostname: 'lh3.googleusercontent.com',
        port: '',
        pathname: '/**',
      },
    ],
  },
};

export default nextConfig;

이후

nextjs는 서버에서 페이지가 만들어 나가기 때문에 loading이 필요 없을 줄 알았는데

Streaming 이라는 형태로 전달 된다고 한다.

CSR과는 아래와 같은 차이가 있는데

nextjs 참 잘 만든 것 같다.

1. CSR (Client-Side Rendering)

클라이언트 사이드 렌더링에서는 컴포넌트가 직접 로딩 상태를 관리합니다:

'use client';

function NewsList() {
  const [news, setNews] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch('/api/news')
      .then(res => res.json())
      .then(data => {
        setNews(data);
        setLoading(false);
      });
  }, []);

  if (loading) return <div>Loading...</div>;
  return <div>{news.map(item => <NewsItem key={item.id} {...item} />)}</div>;
}

2. Server Component Streaming

서버 컴포넌트에서는 Next.js가 자동으로 로딩 상태를 처리합니다:

// app/news/page.jsx
async function NewsPage() {
  const news = await getNews();
  return <NewsList news={news} />;
}

// app/news/loading.jsx
export default function Loading() {
  return <div>Loading...</div>;
}

주요 차이점

1. 초기 로드

  • CSR:
    • 빈 페이지 로드
    • JavaScript 다운로드
    • 로딩 UI 표시
    • 데이터 fetch
    • 최종 컨텐츠 표시
  • Streaming:
    • 로딩 UI가 포함된 HTML 즉시 전송
    • 서버에서 데이터 준비
    • 준비된 컨텐츠로 자동 대체

2. 네트워크 통신

  • CSR: 여러 번의 네트워크 왕복 필요
  • Streaming: 단일 연결을 통한 점진적 전송

3. SEO

  • CSR: 초기에는 빈 컨텐츠만 제공
  • Streaming: 완성된 HTML 제공으로 SEO 친화적

4. 사용자 경험

  • CSR: 상태 변경 시 깜빡임 현상 가능
  • Streaming: 부드러운 컨텐츠 전환

언제 무엇을 사용할까?

CSR이 적합한 경우

  • 실시간 데이터 업데이트가 필요할 때
  • 사용자 인터랙션이 많은 경우
  • 작은 데이터를 자주 갱신할 때

Streaming이 적합한 경우

  • SEO가 중요한 페이지
  • 대량의 데이터를 로드할 때
  • 초기 로딩 성능이 중요할 때

Next.js의 동적 라우트 차이점 이해하기

Next.js에서 동적 라우트를 구현할 때 @slug[[...filter]]는 다르게 동작합니다.

1. @slug 동적 라우트

기본 URL인 /archive는 포함하지 않고, 파라미터가 있는 경우만 처리합니다:

// 라우팅 동작
/archive          ❌ 포함 안됨
/archive/2023     ✅ 포함
/archive/2024     ✅ 포함

2. [[...filter]] Optional Catch-all 라우트

기본 URL을 포함한 모든 하위 경로를 처리합니다:

// 라우팅 동작
/archive          ✅ 포함됨 (params.filter는 undefined)
/archive/2023     ✅ 포함 (params.filter는 ['2023'])
/archive/2023/12  ✅ 포함 (params.filter는 ['2023', '12'])

폴더 구조 예시

app/
  archive/
    page.js            // /archive 처리
    @archive/
      [year]/          // /archive/2023 처리
        page.jsx
      layout.jsx

이렇게 구성하면 기본 경로와 동적 경로를 깔끔하게 분리할 수 있습니다.

+ Recent posts