For-digital-divide는 모바일 환경에서 사용하던 금융 서비스를 PC 환경에서도 동일하게 이용할 수 있도록 돕는것을 목적으로 작업을 시작 했습니다. 이 서비스는 모바일 기기에서만 제공되던 금융 서비스의 기능을 PC에서도 사용할 수 있게 하여, 디지털 격차를 해소하는 것을 목표로 합니다.
사용자에게 이 서비스에서는 어떠한 단계가 있고 현재 진행 중인 단계와 남아 있는 단계를 사용자에게 전달하기 위하여 제작한 Stepper 입니다.
사용자 여정의 투명성 제공: 복잡한 회원가입 및 인증 프로세스에서 사용자가 현재 어느 단계에 있는지, 얼마나 더 진행해야 하는지를 명확하게 보여줍니다. 이를 통해 사용자의 불안감을 해소하고 완료까지의 예상 시간을 인지할 수 있도록 돕습니다.
진행 상황 실시간 피드백: 각 단계의 완료 상태를 시각적으로 표현하여 사용자가 성취감을 느낄 수 있도록 하며, 현재 진행 중인 단계에는 로딩 애니메이션을 통해 시스템이 작동 중임을 알려줍니다.
계층적 단계 구조 표현: 일부 단계(3, 4번)를 들여쓰기로 표현하여 주요 단계와 하위 단계를 구분함으로써, 복잡한 프로세스를 논리적으로 구조화하여 이해하기 쉽게 만듭니다.
상태 관리 및 반응성: Jotai 상태 관리 라이브러리를 활용하여 stepperAtom
을 통해 전역 상태를 관리하며, 경로 변화에 따라 동적으로 단계를 업데이트하는 useStepper
훅을 사용합니다.
조건부 렌더링 최적화: 대시보드 페이지에서는 별도의 완료 상태만 표시하고, 일반 프로세스에서는 전체 단계를 보여주는 조건부 렌더링을 통해 컨텍스트에 맞는 정보만 제공합니다.
접근성 고려 설계: <ol>
태그를 사용한 시맨틱 마크업으로 스크린 리더 사용자도 단계별 순서를 이해할 수 있도록 하였으며, user-select: none
으로 의도치 않은 텍스트 선택을 방지합니다.
시각적 피드백 시스템: 완료된 단계는 체크 아이콘과 색상 변화로, 진행 중인 단계는 로딩 스피너로, 미완료 단계는 투명도 조절로 각각 다른 시각적 상태를 제공하여 직관적인 이해를 돕습니다.
![]() |
![]() |
사용자에게 어떤 로그인을 수행할 것인지에 대한 선택지를 제공하는 페이지입니다. Pin 로그인의 경우 Pin 번호가 등록된 경우와 등록되지 않은 경우에 따라 카드가 변경되는 애니메이션을 제작하였습니다. CSS를 활용한 애니메이션을 적용함으로써 단순한 선택 창이 아닌 흥미를 이끌 수 있는 UI를 제작하고자 하였습니다.
직관적인 선택 경험 제공: 이메일 로그인과 PIN 번호 로그인 두 가지 옵션을 명확하게 구분하여 제시하고, 각각의 특성을 아이콘과 텍스트로 즉시 인식할 수 있도록 설계했습니다. 사용자는 복잡한 설명 없이도 자신에게 적합한 로그인 방식을 빠르게 선택할 수 있습니다.
상황별 적응형 인터페이스: PIN 로그인 카드는 사용자의 디바이스 등록 상태에 따라 다른 시각적 피드백을 제공합니다. 등록된 디바이스가 있는 경우 활성화된 상태로, 그렇지 않은 경우 비활성화 상태와 안내 메시지를 표시하여 사용자가 현재 상황을 명확히 이해할 수 있도록 돕습니다.
인터랙티브한 시각적 피드백: 마우스 호버 시 실시간으로 따라오는 그라디언트 효과와 부드러운 애니메이션을 통해 단순한 클릭 인터페이스를 인터랙티브한 경험으로 전환했습니다. 이는 사용자의 참여도를 높이고 서비스에 대한 긍정적인 첫인상을 형성합니다.
마우스 추적 애니메이션: CardLayout
컴포넌트에서 mousemove
이벤트를 활용하여 실시간으로 마우스 좌표를 CSS 커스텀 프로퍼티(--mx
, --my
)로 전달하고, 이를 통해 radial-gradient
의 중심점을 동적으로 이동시켜 마우스를 따라오는 빛 효과를 구현했습니다.
조건부 컴포넌트 렌더링: 디바이스 등록 상태에 따라 Link
컴포넌트와 일반 div
컴포넌트를 조건부로 렌더링하여, 클릭 가능 여부를 명확히 구분합니다. 또한 아이콘도 상태에 따라 GridIcon
(활성)과 Grid2X2PlusIcon
(비활성)으로 다르게 표시됩니다.
스켈레톤 UI 패턴: PIN 카드의 경우 디바이스 상태 확인 중에는 CardSkeleton
컴포넌트를 표시하여 로딩 상태를 시각적으로 표현하고, 콘텐츠 로딩 완료 후 부드러운 전환 효과를 제공합니다.
접근성 고려 설계: tabIndex
속성과 키보드 네비게이션을 지원하며, 비활성 상태에서는 pointer-events: none
을 적용하여 의도치 않은 상호작용을 방지합니다. 또한 색상과 아이콘을 함께 사용하여 색맹 사용자도 상태를 구분할 수 있도록 했습니다.
성능 최적화: React.memo
를 활용한 불필요한 리렌더링 방지, dynamic import
를 통한 코드 스플리팅으로 초기 로딩 시간을 단축했으며, CSS-in-JS를 활용한 런타임 스타일링으로 상태별 동적 스타일링을 효율적으로 구현했습니다.
반응형 디자인: Container Query와 뷰포트 단위를 활용하여 다양한 화면 크기에서도 일관된 사용자 경험을 제공하며, 터치 디바이스에서도 적절한 피드백을 제공하도록 설계했습니다.
![]() |
![]() |
웹에서 모바일 디바이스 프레임을 구현하고 History Stack을 활용하여 모바일에서 이용이 가능한 뒤로가기 버튼과 홈 버튼의 기능을 구현하였습니다.
네이티브 모바일 경험의 웹 재현: 실제 모바일 디바이스와 동일한 네비게이션 패턴을 웹에서 구현하여 사용자가 친숙한 인터페이스로 서비스를 이용할 수 있도록 했습니다. 특히 디지털 격차 해소라는 서비스 목적에 맞게, 모바일 기기에 익숙한 사용자들이 웹 환경에서도 동일한 조작감을 느낄 수 있도록 설계했습니다.
예측 가능한 네비게이션 제공: 브라우저의 기본 뒤로가기 기능 대신 커스텀 히스토리 스택을 구현하여 일관된 네비게이션 경험을 제공합니다. 사용자는 언제든지 이전 단계로 돌아갈 수 있다는 안정감을 가지며, 복잡한 프로세스를 진행할 때도 부담 없이 탐색할 수 있습니다.
직관적인 제스처 기반 인터페이스: 모바일 기기의 물리적 버튼을 시각적으로 재현하여 터치 인터페이스에 익숙한 사용자들이 즉시 이해할 수 있는 UI를 제공합니다. 홈 버튼과 뒤로가기 버튼의 위치와 디자인을 실제 모바일 기기와 유사하게 구현했습니다.
Zod를 활용한 타입 안전 히스토리 관리: URL 형식 검증과 히스토리 데이터 구조 검증을 통해 런타임 에러를 방지하고 데이터 무결성을 보장합니다. HistoryUrlSchema
와 HistoryStoreSchema
를 통해 잘못된 URL이나 손상된 히스토리 데이터로 인한 오류를 사전에 차단합니다.
SessionStorage 기반 영속성: atomWithStorage
와 커스텀 SyncStorage
구현체를 통해 세션 동안 히스토리를 보존하면서도, 페이지 새로고침 시에도 사용자의 네비게이션 상태를 유지합니다. 또한 localStorage가 아닌 sessionStorage를 사용하여 탭별로 독립적인 히스토리를 관리합니다.
Jotai Atom 패턴을 활용한 상태 분리: 읽기 전용 atom(currentHistoryItemAtom
, readHistoryAtom
)과 쓰기 전용 atom(addHistoryAtom
, clearHistoryAtom
등)을 분리하여 관심사를 명확히 구분하고, 컴포넌트에서 필요한 기능만 선택적으로 사용할 수 있도록 했습니다.
중복 제거 및 용량 제한: 히스토리 추가 시 filter
와 indexOf
를 활용한 중복 URL 제거와 slice(-MAX_HISTORY_LENGTH)
를 통한 최대 10개 항목 제한으로 메모리 효율성을 보장합니다. 이는 장시간 사용 시에도 성능 저하를 방지합니다.
조건부 네비게이션 로직: canGoPrev
, canGoNext
계산을 통해 불가능한 네비게이션 시도를 사전에 차단하고, 경계 조건에서의 안전한 동작을 보장합니다. 이는 사용자가 혼란스러운 상황에 놓이지 않도록 합니다.
접근성 고려 설계: aria-label
과 VisuallyHidden
컴포넌트를 활용하여 스크린 리더 사용자도 네비게이션 버튼의 기능을 명확히 이해할 수 있도록 했으며, 키보드 네비게이션도 지원합니다.
에러 복구 메커니즘: 히스토리 데이터 파싱 실패 시 초기 상태로 복구하는 fallback 로직을 구현하여 예외 상황에서도 서비스 중단 없이 계속 이용할 수 있도록 보장합니다.
인터렉션에 맞*직관적인 선택 경험 제공**: 이메일 로그인과 PIN 번호 로그인 두 가지 옵션을 명확하게 구분하여 제시하고, 각각의 특성을 아이콘과 텍스트로 즉시 인식할 수 있도록 설계했습니다. 사용자는 복잡한 설명 없이도 자신에게 적합한 로그인 방식을 빠르게 선택할 수 있습니다.
상황별 적응형 인터페이스: PIN 로그인 카드는 사용자의 디바이스 등록 상태에 따라 다른 시각적 피드백을 제공합니다. 등록된 디바이스가 있는 경우 활성화된 상태로, 그렇지 않은 경우 비활성화 상태와 안내 메시지를 표시하여 사용자가 현재 상황을 명확히 이해할 수 있도록 돕습니다.
인터랙티브한 시각적 피드백: 마우스 호버 시 실시간으로 따라오는 그라디언트 효과와 부드러운 애니메이션을 통해 단순한 클릭 인터페이스를 인터랙티브한 경험으로 전환했습니다. 이는 사용자의 참여도를 높이고 서비스에 대한 긍정적인 첫인상을 형성합니다.
마우스 추적 애니메이션: CardLayout
컴포넌트에서 mousemove
이벤트를 활용하여 실시간으로 마우스 좌표를 CSS 커스텀 프로퍼티(--mx
, --my
)로 전달하고, 이를 통해 radial-gradient
의 중심점을 동적으로 이동시켜 마우스를 따라오는 빛 효과를 구현했습니다.
조건부 컴포넌트 렌더링: 디바이스 등록 상태에 따라 Link
컴포넌트와 일반 div
컴포넌트를 조건부로 렌더링하여, 클릭 가능 여부를 명확히 구분합니다. 또한 아이콘도 상태에 따라 GridIcon
(활성)과 Grid2X2PlusIcon
(비활성)으로 다르게 표시됩니다.
스켈레톤 UI 패턴: PIN 카드의 경우 디바이스 상태 확인 중에는 CardSkeleton
컴포넌트를 표시하여 로딩 상태를 시각적으로 표현하고, 콘텐츠 로딩 완료 후 부드러운 전환 효과를 제공합니다.
접근성 고려 설계: tabIndex
속성과 키보드 네비게이션을 지원하며, 비활성 상태에서는 pointer-events: none
을 적용하여 의도치 않은 상호작용을 방지합니다. 또한 색상과 아이콘을 함께 사용하여 색맹 사용자도 상태를 구분할 수 있도록 했습니다.
성능 최적화: React.memo
를 활용한 불필요한 리렌더링 방지, dynamic import
를 통한 코드 스플리팅으로 초기 로딩 시간을 단축했으며, CSS-in-JS를 활용한 런타임 스타일링으로 상태별 동적 스타일링을 효율적으로 구현했습니다.
반응형 디자인: Container Query와 뷰포트 단위를 활용하여 다양한 화면 크기에서도 일관된 사용자 경험을 제공하며, 터치 디바이스에서도 적절한 피드백을 제공하도록 설계했습니다.
![]() |
![]() |
웹에서 모바일 디바이스 프레임을 구현하고 History Stack을 활용하여 모바일에서 이용이 가능한 뒤로가기 버튼과 홈 버튼의 기능을 구현하였습니다.
네이티브 모바일 경험의 웹 재현: 실제 모바일 디바이스와 동일한 네비게이션 패턴을 웹에서 구현하여 사용자가 친숙한 인터페이스로 서비스를 이용할 수 있도록 했습니다. 특히 디지털 격차 해소라는 서비스 목적에 맞게, 모바일 기기에 익숙한 사용자들이 웹 환경에서도 동일한 조작감을 느낄 수 있도록 설계했습니다.
예측 가능한 네비게이션 제공: 브라우저의 기본 뒤로가기 기능 대신 커스텀 히스토리 스택을 구현하여 일관된 네비게이션 경험을 제공합니다. 사용자는 언제든지 이전 단계로 돌아갈 수 있다는 안정감을 가지며, 복잡한 프로세스를 진행할 때도 부담 없이 탐색할 수 있습니다.
직관적인 제스처 기반 인터페이스: 모바일 기기의 물리적 버튼을 시각적으로 재현하여 터치 인터페이스에 익숙한 사용자들이 즉시 이해할 수 있는 UI를 제공합니다. 홈 버튼과 뒤로가기 버튼의 위치와 디자인을 실제 모바일 기기와 유사하게 구현했습니다.
Zod를 활용한 타입 안전 히스토리 관리: URL 형식 검증과 히스토리 데이터 구조 검증을 통해 런타임 에러를 방지하고 데이터 무결성을 보장합니다. HistoryUrlSchema
와 HistoryStoreSchema
를 통해 잘못된 URL이나 손상된 히스토리 데이터로 인한 오류를 사전에 차단합니다.
SessionStorage 기반 영속성: atomWithStorage
와 커스텀 SyncStorage
구현체를 통해 세션 동안 히스토리를 보존하면서도, 페이지 새로고침 시에도 사용자의 네비게이션 상태를 유지합니다. 또한 localStorage가 아닌 sessionStorage를 사용하여 탭별로 독립적인 히스토리를 관리합니다.
Jotai Atom 패턴을 활용한 상태 분리: 읽기 전용 atom(currentHistoryItemAtom
, readHistoryAtom
)과 쓰기 전용 atom(addHistoryAtom
, clearHistoryAtom
등)을 분리하여 관심사를 명확히 구분하고, 컴포넌트에서 필요한 기능만 선택적으로 사용할 수 있도록 했습니다.
중복 제거 및 용량 제한: 히스토리 추가 시 filter
와 indexOf
를 활용한 중복 URL 제거와 slice(-MAX_HISTORY_LENGTH)
를 통한 최대 10개 항목 제한으로 메모리 효율성을 보장합니다. 이는 장시간 사용 시에도 성능 저하를 방지합니다.
접근성 고려 설계: aria-label
과 VisuallyHidden
컴포넌트를 활용하여 스크린 리더 사용자도 네비게이션 버튼의 기능을 명확히 이해할 수 있도록 했으며, 키보드 네비게이션도 지원합니다.
에러 복구 메커니즘: 히스토리 데이터 파싱 실패 시 초기 상태로 복구하는 fallback 로직을 구현하여 예외 상황에서도 서비스 중단 없이 계속 이용할 수 있도록 보장합니다.
![]() |
![]() |
쿠키에 등록되어 있는 사용자의 기기정보를 바탕으로 데이터베이스에서 PIN 번호를 조회 후 비교하여 로그인을 수행합니다. 또한 PIN 번호에 대한 데이터는 이미지 데이터와 숫자가 아닌 암호화된 데이터를 기반으로 값을 입력 받아 로그인을 수행합니다.
직관적인 모바일 네이티브 경험: 실제 스마트폰의 잠금해제 패턴을 웹에서 재현하여 디지털 기기에 익숙한 사용자들이 자연스럽게 사용할 수 있도록 설계했습니다. 특히 고령층이나 디지털 초보자들도 복잡한 비밀번호 입력 없이 간단한 4자리 숫자로 빠르게 인증할 수 있습니다.
시각적 피드백을 통한 안정감 제공: PIN 입력 시 각 자리별 시각적 표시와 입력 상태 피드백을 통해 사용자가 현재 진행 상황을 명확히 인지할 수 있도록 했습니다. 또한 입력 오류 시 즉각적인 피드백과 재시도 기회를 제공하여 사용자의 불안감을 최소화합니다.
동적 키패드 레이아웃 셔플링: 매번 로그인 시 숫자 키패드의 배치를 무작위로 변경하여 어깨너머 엿보기(Shoulder Surfing) 공격을 방지합니다. shuffleArray
함수를 통해 0~9 숫자의 위치가 매번 달라지므로, 키 입력 패턴만으로는 PIN을 추측할 수 없습니다.
SHA-256 해시 기반 입력 암호화: 실제 숫자 대신 crypto.createHash('sha256')
으로 생성된 해시값을 클라이언트에서 전송하여 네트워크 패킷 스니핑으로도 실제 PIN 번호를 알 수 없도록 보호합니다. Base64 인코딩을 추가로 적용하여 전송 데이터의 안전성을 강화했습니다.
기기 바인딩 인증: 영구 쿠키에 저장된 고유 device_id
와 데이터베이스의 provider_uid
를 매칭하여 등록된 기기에서만 PIN 로그인이 가능하도록 제한합니다. 이는 PIN이 유출되더라도 다른 기기에서는 접근할 수 없도록 하는 2차 보안 장벽 역할을 합니다.
세션 기반 상태 관리: PIN 인증 성공 후 session_id
와 device_id
를 연결한 세션 쿠키를 생성하여 재인증 없이 서비스를 이용할 수 있도록 하면서도, 세션 종료 시 자동으로 보안 상태가 초기화됩니다.
비동기 인증 플로우: useActionState
를 활용한 비동기 form action으로 사용자 경험을 해치지 않으면서도 서버 사이드에서 안전한 인증 처리를 수행합니다. 인증 실패 시에도 상태를 유지하여 사용자가 처음부터 다시 시작할 필요가 없습니다.
이미지 스프라이트 기반 키패드: 실제 숫자 텍스트 대신 SVG 스프라이트를 사용하여 OCR(광학 문자 인식) 공격을 방지하고, CSS background-position
을 통해 동적으로 숫자를 표시합니다. 이는 자동화된 공격 도구가 화면의 숫자를 인식하기 어렵게 만듭니다.
메모리 보안 최적화: PIN 입력 후 즉시 입력 상태를 초기화하고, 컴포넌트 언마운트 시 관련 데이터를 완전히 정리하여 메모리 덤프를 통한 정보 유출 가능성을 최소화했습니다.
![]() |
![]() |
사용자의 계좌에 대한 정보와 사용자가 지금까지 수행한 거래 내역의 입출금 내역의 차이에 대한 데이터를 시각적인 그래프로 표현하여 확인할 수 있는 Dashboard를 구현하였습니다.
직관적인 금융 정보 시각화: 복잡한 금융 데이터를 D3.js 기반의 인터랙티브 차트로 변환하여 사용자가 한눈에 자신의 금융 상태를 파악할 수 있도록 했습니다. 1개월/3개월/6개월 단위로 기간을 선택할 수 있어 단기적 소비 패턴부터 중장기적 자산 변화까지 다각도로 분석할 수 있습니다.
개인화된 계좌 관리 경험: Carousel 컴포넌트를 통해 여러 계좌를 스와이프로 쉽게 전환할 수 있으며, 각 계좌별로 독립적인 거래 내역과 차트를 제공하여 목적별 자금 관리가 가능합니다. 계좌 타입(입출금, 적금, 신용, 대출)에 따라 다른 시각적 표현과 거래 패턴 분석을 제공합니다.
프로액티브 보안 안내: PIN 미등록 사용자에게는 AlertDialog를 통해 간편 로그인 설정을 권장하여 보안성과 편의성을 동시에 향상시킬 수 있도록 안내합니다. 이는 사용자가 보안 설정을 놓치지 않도록 도와주는 선제적 UX입니다.
제너레이터 기반 스트리밍 데이터 처리: getTransactions
함수에서 Generator 패턴을 활용하여 대용량 거래 내역을 청크 단위(100개씩)로 처리합니다. 이는 메모리 효율성을 높이고 대량의 거래 데이터가 있어도 안정적인 성능을 보장합니다.
함수형 프로그래밍 패러다임: 책을 읽고 배운 fx
라이브러리를 활용한 함수형 데이터 변환 파이프라인으로 복잡한 데이터 가공 과정을 간결하고 안전하게 처리합니다. map
, filter
, chunk
등의 연산을 체이닝하여 가독성과 유지보수성을 향상시켰습니다.
타입 안전성 보장: TypeScript와 Zod를 조합하여 컴파일 타임과 런타임 모두에서 타입 안전성을 보장합니다. 특히 BigInt로 저장된 계좌번호와 거래금액을 Number로 안전하게 변환하는 과정에서 데이터 손실이나 오버플로우를 방지합니다.
컴포넌트 최적화: key
prop을 계좌번호로 설정하여 계좌 변경 시 TransactionHistorySection이 완전히 재렌더링되도록 하여 데이터 일관성을 보장하고, 메모화된 컴포넌트들을 통해 불필요한 리렌더링을 방지합니다.

참조 테이블을 통한 정규화: account_types와 transaction_types 같은 테이블을 분리하여 code로 관리합니다. 이는 데이터의 중복을 제거하고 일관성을 보장하며, 향후 새로운 유형이 추가될 때 유지보수 및 확장을 용이하게 합니다.
외래 키를 활용한 관계 무결성 보장: FOREIGN KEY 제약조건과 ON DELETE CASCADE 옵션을 사용하여 사용자나 계좌 삭제 시 관련된 모든 데이터(거래 내역, 인증 정보 등)가 자동으로 정리되도록 설계했습니다. 이를 통해 데이터베이스 내에 고아 레코드(Orphaned Record)가 발생하는 것을 방지하고 데이터 무결성을 유지합니다.
조인 및 조회 성능 향상을 위해 주요 외래 키에 인덱스(Index)를 생성하여 대규모 데이터 환경에서도 안정적인 성능을 보장합니다.
역할 분리를 통한 유연한 구조: users 테이블과 auth_methods 테이블을 1:1 관계로 분리하여 사용자 고유 정보와 인증 수단을 독립적으로 관리합니다. 이 구조는 향후 소셜 로그인이나 다른 인증 방식이 추가되더라도 기존 users 테이블의 변경 없이 유연하게 시스템을 확장할 수 있게끔 했습니다.