히치키치

다국어 서비스에서 실용적인 사용자 중심 에러 처리 본문

Blockie

다국어 서비스에서 실용적인 사용자 중심 에러 처리

히치키치 2025. 8. 15. 17:32
완벽한 에러 처리보다 사용자가 길을 잃지 않는 것이 더 중요하다

 

배경

Next.js 14 App Router와 next-intl을 사용해 다국어 서비스 개발 중, 사용자가 잘못된 URL에 접근하거나 지원하지 않는 언어로 접속할 때의 처리 방법에 대해 고민함

 

다양한 잘못된 접근 패턴들

  • 존재 하지 않는 페이지 접근 : /invalid-page, /random/path 등 
  • 지원하지 않는 locale 접근 : /fr/home
  • 잘못된 경로 구조 : /ko/invalid/nested/path
  • ChunkLoadError, PermissionError, RuntimeError 등

핵심 아이디어

  • 복잡한 에러 분류에 대한 각각의 해결 방안 고안 X
  • 해당 에러를 마주한 사용자의 위치를 서비스 진입 경로로 재연결시켜 서비스 이용 여정을 이어나가게 함
  • 개발 리소스 대비 최대 효과 추구
  • 짧은 시간 구현으로 사용자 이탈을 방지하고, 서비스 핵심 플로우로 자연스럽게 유도하는 것

비즈니스 로직과 사용자 상태 기반 복구 전략

  • 미인증 사용자 → 로그인 페이지 (서비스 진입점으로 유도)
  • 인증된 사용자 → 메인 서비스 (핵심 기능(/cube 페이지)으로 복귀)

Nextjs App router 폴더 구조 이해

app/
├── [locale]/
│   ├── layout.tsx          # locale 검증
│   ├── page.tsx           # 루트 페이지
│   ├── not-found.tsx      # 404 페이지
│   └── [...rest]/         # catch-all
│       └── page.tsx
├── error.tsx              # 에러 바운더리
└── global-error.tsx       # 전역 에러 처리

 

1. 파일 기반 라우팅 시스템

  • app/[locale]/ → 다국어 동적 라우팅
  • app/[locale]/page.tsx → 각 locale의 루트 페이지
  • app/[locale]/cube/page.tsx → 각 locale의 주요 서비스 페이지
  • app/[locale]/[...rest]/ → catch-all 라우팅 (모든 하위 경로 포착)

2. 에러 처리 계층 구조

  • global-error.tsx → 최상위 전역 에러 (앱 전체)
  • error.tsx → 루트 레벨 에러
  • [locale]/layout.tsx → locale 레벨 검증
  • not-found.tsx → 404 전용 처리

3. 라우팅 우선 순위

  • [locale]/page.tsx → 정확한 매칭 우선
  • [locale]/cube/page.tsx → 정적 경로 우선
  • [locale]/[...rest]/page.tsx → 마지막 catch-all

4. 실제 서비스 flow

[정상적 요청인 경우]

1. middleware.ts → locale 검증
2. [locale]/layout.tsx → locale 재검증  
3. 매칭되는 page.tsx 제공됨

 

[잘못된 URL 접근인 경우]

1. middleware.ts (통과)
2. [locale]/layout.tsx (통과) 
3. 매칭되는 page.tsx 없음
4. [...rest]/page.tsx 실행 → 인증 상태별 분기
5. not-found.tsx → 복구 메커니즘 제공

 

4단계 방어선 구축

1. 1단계: Middleware

  • 역할: 잘못된 언어 접근 자체 봉쇄
// middleware.ts - 지원하지 않는 locale 필터링
export default createMiddleware(routing);
export const config = {
  matcher: "/((?!api|trpc|_next|_vercel|.*\\..*).*)",
};

 

2. 2단계: 구조적 검증 (Layout)

  • 역할: 1단계 통과한 edge case까지 포착 + 라이브러리 무관 검증 로직 커스텀 가능
// layout.tsx - locale 유효성 재검증
if (!hasLocale(routing.locales, locale)) {
  notFound();
}

 

3. 3단계: 지능형 라우팅 (Catch-All)

  • 역할: 모든 미매칭 경로를 의미있는 목적지로 전환
// [...rest]/page.tsx - 핵심 구현
export default async function CatchAllPage({ params }: { params: Params }) {
  const access_token = cookieStore.get("access_token")?.value ?? null;
  const { locale } = await params;

  // 🎯 핵심 로직: 인증 상태 기반 복구
  if (!access_token) {
    // 미인증 사용자 → 로그인으로 유도
    redirect({ href: pageUrl.signin, locale });
  } else {
    // 인증된 사용자 → 404 페이지 (자동 복구 및 메인 기능 복구 로직이 담긴 페이지)
    return notFound();
  }
}

 

4. 최후 방어 (Error Boundary)

  • 역할: 잘못된 url 입력으로 인한 런타임 에러 상황을 안전하게 처리
// error.tsx/global-error.tsx
export default function Error({ error, reset }: ErrorProps) {
  useEffect(() => {
    logger({
      eventType: EventName.RUNTIME_ERROR,
      // ... 상세 에러 정보 수집
    });
  }, [error]);

  return notFound(); // 통합된 복구 메커니즘으로 연결
}

 

운영 데이터를 위한 로깅 시스템

  • 에러 발생 상황을 운영 개선의 기회으로 활용
  • 자명한 에러 발생 패턴 파악의 필요성
  • 에러 발생 시점과 컨텍스트 정보 수집
    • 에러 발생 URL 패턴 및 사용자 접근 경로
    • 디바이스/브라우저 정보 → 호환성 이슈 파악
    • 에러 발생 빈도 → 시스템 안정성 모니터링
// 일부 예시!
await logger({
  eventType: EventName.NOT_FOUND,
  userAgent: headerList.get("user-agent"),
  request: {
    url: fullUrl,
    method: ApiMethod.get,
  },
  error: {
    name: EventName.NOT_FOUND,
    message: `No matched Page under [locale]`,
    stack: `apps/web/app/[locale]/[...rest]/page.tsx`,
    source: fullUrl,
  },
});

 

능동적인 서비스 이용 여정 복구 로직

사용자 심리

  • 기술적 완성도보다 사용자가 서비스를 계속 이용할 수 있는지에 집중
  • 어떤 에러가 발생했는지는 사실 관심없고 그래서 어캐하면 계속 할 수 있는지가 중요함
  • "어? 뭔일이지? 길을 잃었네" → 시스템 응답: "괜찮아요, 이쪽으로 안내할게요"
  • 사용자가 본인을 능동적으로 구출하는 로직 필요함

복구 메커니즘

  1. 즉시 복구: "홈으로 가기" 버튼
  2. 자동 복구: 10초 카운트다운 후 자동 이동
  3. 친화적 안내: 딱딱한 에러가 아닌 도움말 제공
  4. 유지보수: 복구 관련 로직이 응집되어 있어 추후 상황에 맞게 조절 및 작성 용이
// not-found.tsx - 핵심 복구 메커니즘

export default function NotFoundPage() {
  const [countdown, setCountdown] = useState(10);
  const router = useRouter();

  useEffect(() => {
    // 10초 후 자동 홈페이지 이동
    const timer = setInterval(() => {
      setCountdown((prev) => {
        if (prev <= 1) {
          router.push("/cube"); // 메인 서비스로 복구
          return 0;
        }
        return prev - 1;
      });
    }, 1000);
    return () => clearInterval(timer);
  }, [router]);

  return (
    <div>
      {/* 친근한 404 UI */}
      <div className="text-6xl font-bold text-gray-300">404</div>
      
      {/* 즉시 복구 옵션 */}
      <Button onClick={() => router.push(pageUrl.cube)}>
        홈으로 가기
      </Button>
      
      {/* 자동 복구 안내 */}
      {countdown > 0 && (
        <p>{countdown}초 후 홈페이지로 자동 이동됩니다.</p>
      )}
    </div>
  );
}

 

효과

1. 개발 효율성

  • 30분 구현으로 99% 에러 케이스 해결
  • 복잡한 에러 분류 대신 명확한 비즈니스 로직 적용
  • 유지보수하기 쉬운 단순한 구조

2. 사용자 경험 개선

  • before: 에러 페이지 막다른 길
  • after: 자동/수동 복구 옵션이 담겨 있어 서비스 지속적 이용 가능

3. 비지니스 임팩트

  • 사용자 이탈률 감소: 길을 잃고 브라우저 창 꺼버리는 것 방지
  • 서비스 재진입 유도: 로그인 → 메인 기능으로 자연스러운 플로우
  • 장애 상황 최소화: 예상치 못한 URL 접근도 안전하게 처리
Comments