SpeakUp (AI 모의 면접 플랫폼)

Frontend

Next.js의 App Router를 기반으로 서버 컴포넌트와 클라이언트 컴포넌트를 적절히 활용하여, 성능과 사용자 경험을 모두 고려한 인터랙티브 UI를 개발했습니다.

Next.jsReactTypeScriptTailwind CSSDaisyUIHeadless UIZodky

핵심 기여 사항

  • MVVM 아키텍처 도입: UI 로직(ViewModel)과 뷰(View)를 커스텀 훅과 컴포넌트로 분리하여 테스트 용이성과 코드 재사용성을 극대화했습니다.
  • 전역 상태 관리 시스템 설계: React Context API를 활용하여 인증(AuthProvider), 테마(ThemeProvider), 전역 알림(ToastProvider), Drawer UI 등 여러 컴포넌트가 공유하는 상태를 체계적으로 관리했습니다.
  • 고급 UI 인터랙션 구현: IntersectionObserver API를 활용하여 스크롤 위치에 따라 목차가 활성화되는 'Scrollspy' 기능을 구현하고, Tailwind CSS의 반응형 시스템을 통해 데스크탑과 모바일 환경에 최적화된 레이아웃을 구축했습니다.
  • 안전한 API 클라이언트 설계: ky 라이브러리의 hooks를 이용, Access Token 만료 시 자동으로 Refresh Token을 사용해 토큰을 재발급하고 원래 요청을 재시도하는 인터셉터를 구현하여 사용자 경험을 향상시켰습니다.

도전 과제: 복잡성과 확장성을 모두 만족시키는 UI 아키텍처 설계

SpeakUp 프로젝트는 실시간 인증 상태, 사용자별 데이터, 복잡한 이력 관리 UI 등 여러 상태가 유기적으로 얽혀있는 애플리케이션입니다. 초기 단계부터 명확한 아키텍처 없이 개발을 진행했다면, 컴포넌트 간의 의존성이 복잡해지고 상태 관리가 파편화되어 유지보수가 어려운 '스파게티 코드'가 될 위험이 컸습니다. 따라서 저의 핵심 과제는 확장 가능하고, 테스트하기 쉬우며, 동시에 높은 수준의 사용자 경험을 제공하는 견고한 프론트엔드 아키텍처를 설계하는 것이었습니다.

해결 전략 1: MVVM 패턴과 커스텀 훅을 이용한 관심사 분리

UI의 렌더링 로직과 상태 관리 로직을 분리하기 위해 MVVM(Model-View-ViewModel) 아키텍처를 도입했습니다. React 환경에서는 이를 커스텀 훅(Custom Hook)을 통해 효과적으로 구현할 수 있었습니다. View(컴포넌트)는 오직 UI를 그리는 역할만 하고, 모든 상태와 비즈니스 로직은 ViewModel(커스텀 훅)이 담당하도록 하여 각 부분의 책임을 명확히 했습니다.

Frontend Architecture

View (Component)
ViewModel (Custom Hook)
Model (API Service)

Global State

React Context API
ViewModel

[그림 1] MVVM 패턴 기반의 프론트엔드 아키텍처

이러한 구조는 컴포넌트를 재사용하기 쉽게 만들고, UI와 독립적으로 비즈니스 로직을 테스트할 수 있게 하여 코드의 품질과 유지보수성을 크게 향상시켰습니다.

해결 전략 2: "똑똑한" API 클라이언트와 전역 상태 관리

사용자 경험의 핵심은 끊김 없는 인증 세션 유지입니다. 이를 위해 `ky` 라이브러리의 hooks 기능을 활용하여 Access Token 만료 시 자동으로 토큰을 재발급하는 인터셉터를 구현했습니다. 이 "똑똑한" API 클라이언트는 401 에러를 감지하면 Refresh Token으로 새로운 Access Token을 요청하고, 원래 실패했던 API 요청을 사용자가 모르게 재시도하여 매끄러운 사용 경험을 제공합니다.

// lib/apiClient.ts - 토큰 자동 재발급 로직
const clientApi = ky.create({
hooks: {
afterResponse: [
async (request, options, response) => {
// Access Token 만료로 401 에러 발생 시
if (response.status === 401 && !isRefreshing) {
isRefreshing = true;
try {
// Refresh Token으로 새 Access Token 요청
const { access_token } = await fetchNewToken();
setAccessToken(access_token);
// 원래 실패했던 요청을 새 토큰으로 재시도
request.headers.set("Authorization", `Bearer ${access_token}`);
return ky(request);
} catch (error) {
// 리프레시 실패 시 로그아웃 처리
logout();
} finally {
isRefreshing = false;
}
}
return response;
},
],
},
});

또한, 인증(AuthProvider), 테마(ThemeProvider), 전역 알림(ToastProvider) 등 앱 전역에서 필요한 상태는 React Context API 기반의 Provider 패턴으로 설계하여, 어떤 컴포넌트에서도 일관된 상태에 쉽게 접근할 수 있도록 했습니다.

결과 및 성과

체계적인 아키텍처 설계를 통해 복잡한 상태를 가진 애플리케이션을 안정적으로 구축할 수 있었습니다. 특히 MVVM 패턴과 전역 상태 관리 시스템은 향후 새로운 기능이 추가되더라도 코드 베이스가 복잡해지는 것을 방지하는 견고한 기반이 되었습니다. 자동 토큰 재발급 로직은 사용자가 세션 만료로 인해 겪는 불편함을 최소화하여 서비스의 사용성을 크게 향상시켰습니다. 이 프로젝트를 통해 단순히 기능을 구현하는 것을 넘어, 확장성과 유지보수성을 고려한 아키텍처 설계의 중요성을 깊이 체감할 수 있었습니다.