HUNIL PARK

Scholarly Chain

Hyperledger Fabric 블록체인 기반 학생회비 관리 시스템

Next.jsReactTypeScriptTailwind CSSshadcn/uiFirebase
Scholarly Chain

프로젝트 개요

Scholarly Chain은 캡스톤 디자인 과제로 진행한 Hyperledger Fabric 블록체인 기반 학생회비 투명 관리 시스템입니다. 학생회비 수입과 지출 내역을 블록체인에 기록하여 투명성을 확보하고, 학생들이 학생회비 사용 내역을 실시간으로 확인할 수 있도록 하는 것이 목표였습니다. Next.js, React, TypeScript, Tailwind CSS, shadcn/ui를 기반으로 한 프론트엔드를 100% 담당했으며, Firebase Cloud Messaging을 활용한 역할별 푸시 알림과 JWT 기반 인증을 구현했습니다. 백엔드는 Hyperledger Fabric 블록체인 네트워크와 연동되어 있었고, 프론트엔드는 백엔드 API를 호출하여 블록체인 데이터를 사용자에게 제공하는 역할을 수행했습니다.

4인 팀에서 프론트엔드 개발을 전담했으며, 15개 이상의 페이지와 30개 이상의 재사용 가능한 컴포넌트를 제작했습니다. JWT 토큰 자동 갱신 미들웨어를 구현하여 사용자 세션 끊김을 방지했고, 학생, 위원, 관리자의 3가지 역할에 따라 서로 다른 대시보드와 기능을 제공하는 역할 기반 UI 시스템을 설계했습니다. shadcn/ui 컴포넌트 라이브러리를 활용하여 일관된 디자인 시스템을 구축했으며, Firebase Cloud Messaging을 연동하여 학생회비 입금 확인, 지출 승인 등의 알림을 역할에 따라 선택적으로 전송하는 푸시 알림 시스템을 구현했습니다. Next.js API Routes를 프록시로 활용하여 백엔드 API 호출을 중계했고, Vercel에 배포하여 실제 서비스 환경을 구성했습니다.

Scholarly Chain architecture

기술 구현

JWT 자동 갱신 미들웨어

사용자가 대시보드를 사용하는 중에 JWT 액세스 토큰이 만료되면 API 호출이 실패하고 세션이 끊겨 다시 로그인해야 하는 문제가 있었습니다. 특히 학생회 관리자가 장시간 지출 내역을 검토하거나 승인 작업을 수행할 때 토큰 만료로 인한 중단은 사용자 경험을 크게 저하시켰습니다. 액세스 토큰은 보안상 짧은 유효 기간을 가져야 하지만, 매번 사용자에게 재로그인을 요구하는 것은 비현실적이었습니다. 리프레시 토큰을 활용해 자동으로 액세스 토큰을 갱신하는 메커니즘이 필요했습니다.

Next.js의 middleware 기능을 활용하여 모든 API 요청을 가로채고, 액세스 토큰 만료 여부를 확인하는 로직을 구현했습니다. 미들웨어에서 JWT 토큰의 만료 시간을 디코딩하여 검사하고, 만료가 임박하거나 이미 만료된 경우 자동으로 리프레시 토큰을 사용해 새로운 액세스 토큰을 발급받도록 구성했습니다. 리프레시 요청이 성공하면 새로운 토큰을 쿠키에 저장하고, 원래 요청을 재시도하여 사용자가 토큰 갱신 과정을 인지하지 못하도록 했습니다. 만약 리프레시 토큰도 만료되었다면 로그인 페이지로 리디렉션하여 재인증을 요구했습니다.

사용자는 장시간 대시보드를 사용해도 세션이 자동으로 유지되어, 중간에 끊김 없이 작업을 계속할 수 있게 되었습니다. 토큰 갱신 과정이 백그라운드에서 자동으로 처리되어 사용자 경험이 크게 개선되었고, 보안과 편의성을 동시에 확보할 수 있었습니다. 관리자 피드백에서도 '작업 중에 로그아웃되지 않아 편리하다'는 긍정적인 반응을 얻었습니다.

역할 기반 UI 시스템

학생, 위원, 관리자의 3가지 역할은 각각 다른 권한과 기능을 필요로 했습니다. 학생은 학생회비 사용 내역을 조회만 할 수 있고, 위원은 지출 신청을 작성할 수 있으며, 관리자는 지출 승인과 전체 데이터 관리 권한을 가집니다. 모든 사용자가 동일한 화면을 보는 것이 아니라, 역할에 따라 맞춤화된 대시보드와 메뉴를 제공해야 했습니다. 또한 권한이 없는 페이지에 접근을 시도할 경우 이를 차단하고 적절한 피드백을 제공해야 했습니다. 역할별로 서로 다른 컴포넌트를 조건부로 렌더링하면서도 코드 중복을 최소화하고 유지보수성을 유지하는 것이 과제였습니다.

shadcn/ui 컴포넌트 라이브러리를 도입하여 일관된 디자인 시스템을 구축하고, 역할별로 다른 UI를 조건부로 렌더링하는 구조를 설계했습니다. 사용자 인증 시 백엔드에서 받은 역할 정보를 React Context에 저장하고, 모든 컴포넌트에서 현재 사용자의 역할을 참조할 수 있도록 했습니다. 페이지 수준에서는 Next.js의 middleware를 활용해 역할 기반 접근 제어를 구현하여, 권한이 없는 사용자가 특정 페이지에 접근하려 하면 403 에러 페이지로 리디렉션했습니다. 컴포넌트 수준에서는 역할별로 다른 대시보드 레이아웃을 보여주는 RoleBasedDashboard 컴포넌트를 작성하여, 학생용, 위원용, 관리자용 인터페이스를 각각 구성했습니다. shadcn/ui의 Card, Table, Form 등의 컴포넌트를 재사용하여 코드 중복을 줄이고, Tailwind CSS로 역할별 테마 색상을 다르게 적용하여 시각적으로 구분했습니다.

각 역할의 사용자는 자신에게 필요한 기능과 정보만 명확하게 제공받아, 혼란 없이 시스템을 사용할 수 있었습니다. shadcn/ui 덕분에 일관된 디자인 시스템을 유지하면서도 역할별 커스터마이징이 용이했고, 새로운 역할이나 권한을 추가할 때도 기존 구조를 그대로 활용할 수 있었습니다. 사용자 피드백에서 '내가 필요한 기능만 보여서 직관적이다'는 평가를 받았습니다.

FCM 역할별 푸시 알림 시스템

학생회비와 관련된 중요한 이벤트(입금 확인, 지출 신청, 지출 승인 등)가 발생했을 때 사용자에게 실시간으로 알림을 전달해야 했습니다. 하지만 모든 사용자에게 모든 알림을 보내는 것은 불필요하고, 역할에 따라 관련된 알림만 선택적으로 전송해야 했습니다. 예를 들어 지출 신청이 제출되면 관리자에게만 알림을 보내고, 지출이 승인되면 해당 신청을 한 위원에게만 알림을 보내야 했습니다. 웹 환경에서 푸시 알림을 구현하고, 사용자가 알림을 클릭하면 관련 페이지로 이동하는 인터랙션도 필요했습니다.

Firebase Cloud Messaging(FCM)을 연동하여 웹 푸시 알림 기능을 구현했습니다. 사용자가 로그인하면 FCM 토큰을 발급받아 백엔드에 전송하고, 백엔드는 사용자의 역할과 FCM 토큰을 매핑하여 데이터베이스에 저장했습니다. 백엔드에서 특정 이벤트가 발생하면, 해당 이벤트와 관련된 역할의 사용자들에게만 FCM API를 통해 푸시 알림을 전송하도록 구성했습니다. 프론트엔드에서는 Service Worker를 등록하여 백그라운드에서도 푸시 알림을 수신할 수 있도록 했고, 알림을 클릭하면 관련 페이지(예: 지출 승인 대기 목록)로 자동 이동하는 로직을 구현했습니다. 알림 권한 요청 UI도 사용자 친화적으로 디자인하여, 처음 로그인 시 알림 설정을 안내하도록 했습니다.

사용자는 중요한 이벤트를 실시간으로 알림받아 즉각 대응할 수 있게 되었고, 역할별 필터링 덕분에 불필요한 알림이 줄어들어 알림 피로도가 낮았습니다. 관리자는 새로운 지출 신청을 즉시 확인하고 승인 처리할 수 있었으며, 위원은 자신의 신청이 승인되었는지 실시간으로 알 수 있어 업무 효율이 크게 향상되었습니다. FCM을 통한 푸시 알림은 시스템의 반응성과 사용자 참여도를 높이는 핵심 기능으로 자리 잡았습니다.

트러블슈팅

API Route 프록시 패턴에서 발생한 인증 토큰 전달 이슈

프론트엔드에서 백엔드 API를 직접 호출하는 대신 Next.js API Routes를 프록시로 활용하여 CORS 이슈를 회피하고 보안을 강화하려 했습니다. 하지만 클라이언트에서 API Route로 요청을 보낼 때 쿠키에 저장된 JWT 토큰을 함께 전달하고, API Route에서 다시 백엔드로 요청을 보낼 때도 이 토큰을 정확히 전달해야 했는데, 토큰 전달 과정에서 누락되거나 형식이 맞지 않아 인증 실패가 발생하는 문제가 빈번했습니다. 특히 httpOnly 쿠키로 저장된 토큰을 서버 사이드에서 읽어와 백엔드에 전달하는 로직이 복잡했고, 에러 핸들링도 일관되지 않아 디버깅이 어려웠습니다.

API Route에서 들어오는 요청의 쿠키를 파싱하여 JWT 토큰을 추출하고, 이를 Authorization 헤더에 담아 백엔드로 전달하는 표준화된 프록시 함수를 작성했습니다. 모든 API Route는 이 공통 프록시 함수를 사용하도록 구조화하여, 토큰 전달 로직을 중앙화했습니다. 백엔드로부터 401 또는 403 응답이 오면 클라이언트에 동일한 상태 코드와 에러 메시지를 반환하여, 프론트엔드에서 일관된 에러 처리를 할 수 있도록 했습니다. 또한 개발 환경과 프로덕션 환경에서 쿠키 설정(Secure, SameSite)이 달라야 했기 때문에, 환경 변수를 통해 동적으로 쿠키 옵션을 설정하는 유틸리티 함수를 만들었습니다.

API Route 프록시 패턴이 안정화되어 인증 토큰 전달 문제가 해결되었고, CORS 이슈 없이 안전하게 백엔드 API를 호출할 수 있게 되었습니다. 공통 프록시 함수 덕분에 새로운 API 엔드포인트를 추가할 때도 일관된 방식으로 구현할 수 있었고, 에러 핸들링이 표준화되어 디버깅 시간이 크게 단축되었습니다.

shadcn/ui 컴포넌트 커스터마이징과 디자인 시스템 일관성 유지

shadcn/ui는 Radix UI 기반의 헤드리스 컴포넌트 라이브러리로, 스타일링은 개발자가 직접 해야 했습니다. 프로젝트에서 30개 이상의 재사용 컴포넌트를 제작하면서, 각 컴포넌트의 스타일이 일관되지 않고 Tailwind CSS 클래스가 중복되어 관리가 어려웠습니다. 특히 Button, Card, Form 등 기본 컴포넌트를 여러 페이지에서 다르게 커스터마이징하다 보니, 디자인 시스템의 일관성이 깨지고 유지보수가 힘들어졌습니다. 또한 다크 모드를 지원해야 했는데, 모든 컴포넌트에서 다크 모드 스타일을 일일이 지정하는 것은 비효율적이었습니다.

Tailwind CSS의 @layer 기능을 활용하여 커스텀 유틸리티 클래스를 정의하고, shadcn/ui 컴포넌트에서 재사용할 수 있는 공통 스타일 클래스를 생성했습니다. 예를 들어 btn-primary, card-base 같은 추상화된 클래스를 만들어, 모든 버튼과 카드가 동일한 기본 스타일을 갖도록 했습니다. 색상, 간격, 글꼴 크기 등의 디자인 토큰을 Tailwind 설정 파일에 미리 정의하여, 컴포넌트에서는 토큰을 참조하는 방식으로 스타일링했습니다. 다크 모드는 Tailwind의 dark: 접두사를 활용하여 자동으로 전환되도록 구성했고, next-themes 라이브러리로 테마 전환 기능을 구현했습니다. 컴포넌트 라이브러리를 src/components/ui 폴더에 모아두고, 각 컴포넌트의 props와 스타일 옵션을 문서화하여 팀원들이 일관되게 사용할 수 있도록 가이드를 제공했습니다.

디자인 시스템의 일관성이 크게 향상되었고, 새로운 페이지나 기능을 추가할 때도 기존 컴포넌트를 재사용하여 빠르게 개발할 수 있었습니다. Tailwind CSS의 유틸리티 클래스 덕분에 스타일링이 간결해졌고, 다크 모드 전환도 자연스럽게 동작하여 사용자 경험이 향상되었습니다. 컴포넌트 문서화 덕분에 팀원 간 협업이 원활해졌고, 디자인 수정이 필요할 때도 중앙화된 스타일 클래스만 수정하면 되어 유지보수성이 높아졌습니다.

회고

Next.js와 shadcn/ui를 활용한 대규모 프론트엔드 프로젝트를 처음부터 끝까지 주도하면서, 현대적인 프론트엔드 개발 스택에 대한 실무 역량을 크게 강화할 수 있었습니다. JWT 인증 미들웨어와 역할 기반 접근 제어를 구현하며 웹 애플리케이션의 보안과 권한 관리에 대한 깊은 이해를 얻었고, Firebase Cloud Messaging을 통한 푸시 알림 시스템 구축으로 실시간 통신 기술 경험도 쌓았습니다. 특히 4인 팀에서 프론트엔드를 전담하며 15개 이상의 페이지와 30개 이상의 재사용 컴포넌트를 제작한 경험은, 큰 규모의 프로젝트를 독립적으로 설계하고 구현하는 능력을 키우는 데 큰 도움이 되었습니다. shadcn/ui와 Tailwind CSS를 활용한 디자인 시스템 구축 경험은 컴포넌트 재사용성과 일관성을 유지하는 방법을 체득하게 했고, API Route 프록시 패턴을 통해 프론트엔드-백엔드 통신 아키텍처를 설계하는 역량도 향상되었습니다.

프로젝트 초기에 테스트 코드를 작성하지 않아, 기능이 추가될수록 리그레션 버그가 자주 발생했습니다. 특히 인증 로직이나 역할 기반 UI처럼 복잡한 로직은 단위 테스트가 필수적인데, 일정에 쫓겨 생략한 것이 아쉬웠습니다. 다음에는 최소한 핵심 비즈니스 로직에 대한 테스트 커버리지를 확보하여 안정성을 높이고 싶습니다. 또한 상태 관리를 Context API로만 처리하다 보니, 컴포넌트 깊이가 깊어지면서 prop drilling 이슈가 발생했습니다. Zustand나 Jotai 같은 가벼운 상태 관리 라이브러리를 도입했다면 코드가 더 간결하고 유지보수하기 쉬웠을 것입니다. 초기 컴포넌트 설계 시 재사용성을 충분히 고려하지 못해, 나중에 비슷한 컴포넌트를 여러 번 만들어야 했던 점도 개선이 필요합니다. 다음 프로젝트에서는 디자인 단계에서부터 컴포넌트 구조를 체계적으로 설계하고, 테스트와 상태 관리를 초기부터 고려하여 더 견고한 시스템을 구축하고 싶습니다.