← 목록으로

Project Detail

TimeTogether

그룹 약속의 시간 조율 · 장소 선정 · 일정 확정을 하나로 묶은 모바일 웹 클라이언트

2024.03 — 2025.10Frontend 단독 구현Next.js 16 · TypeScript · Zustand · TanStack Query · Web Crypto API

여러 명이 모일 시간을 조율하고, 장소를 결정하고, 일정을 확정하는 전체 흐름을 한 화면 안에서 매끄럽게 잇는 데 집중. 외부 암호 라이브러리 없이 브라우저 표준 Web Crypto API 만으로 종단간 암호화를 구현해, 아이디 · 비밀번호 · 이메일 · 전화번호가 평문으로 서버에 도달하지 않는 구조를 달성.

클라이언트 아키텍처

비밀번호 · 이메일 · 전화번호 같은 민감 정보는 브라우저 안의 암·복호화 모듈을 반드시 거쳐 해시값이나 암호문으로 변환된 뒤에만 서버로 송신됨.

기술 의사결정

Web Crypto API 만으로 클라이언트 사이드 종단간 암호화 구현

Web Crypto APIPBKDF2AES-GCMIndexedDB

Context

그룹 약속 조율 특성상 이메일·전화번호·아이디 같은 식별 가능한 개인정보를 다루어야 함. 서버 DB 가 유출되더라도 원문이 복원되지 않는 구조가 필요했고, 외부 암호 라이브러리를 도입하면 공급망 위험과 번들 비대화가 따라옴.

Problem

일반적인 서버 측 해싱은 로그·메모리 덤프·내부자 접근 등 여러 경로로 평문이 노출될 수 있음. 반대로 사용자 키를 브라우저 메모리에만 두면 새로고침마다 재로그인이 필요해 UX 가 무너짐.

Action

외부 의존성 없이 브라우저 표준 crypto.subtle 로 모든 암·복호화를 처리. 비밀번호로부터 PBKDF2 100k 회 반복을 거쳐 사용자 키를 유도하고, 인증용 해시는 다시 PBKDF2 200k 회로 분리해 키와 인증 해시가 같은 출처를 공유하지 않도록 설계. 개인정보는 매 회 새로 생성한 12바이트 IV 와 함께 AES-GCM 으로 암호화하고, 사용자 키 자체는 IndexedDB 에 추출 불가(extractable: false) CryptoKey 로 보관해 XSS 가 성공해도 키 바이트 추출 자체가 브라우저 레벨에서 차단되도록 함.

Result

원본 비밀번호·이메일·전화번호의 서버 전송 0 회 달성. 외부 암호 라이브러리 의존성 0 개로 공급망 공격 표면 제거. 새로고침 시 키 복원은 IndexedDB 에서 즉시 수행되어 사용자 개입 0 회로 세션 유지.

그룹 키 공유와 다단계 암호화 식별자로 그룹 익명성 보장

AES-GCMReact Query암호학적 접근 제어

Context

단일 응답으로 그룹 정보와 멤버 목록을 평문 반환하면 서버 측에서 누가 누구와 같은 그룹인지 추적할 수 있고, 이는 사교 관계 프로파일링으로 이어짐. 동시에 초대·조회 흐름은 매끄러워야 했음.

Problem

그룹 키를 클라이언트 단독 보관 시 멤버 초대가 불가능. 서버 평문 보관 시 익명성 전제가 무너짐. 그룹 키와 멤버 식별자를 모두 서버에서 식별 불가 상태로 유지하면서 초대·조회는 자연스럽게 동작해야 함.

Action

그룹 생성 시 클라이언트가 AES-GCM 256-bit 그룹 키를 생성하고, 본인의 사용자 키로 한 번 더 감싸 서버에 올림. 초대 시에는 그룹 키와 그룹 식별자가 결합된 토큰을 초대 URL 에 실어 그룹원 사이에서만 공유. 멤버 가입 시 본인의 사용자 ID 를 그룹 키로 암호화한 뒤, 다시 본인의 사용자 키로 한 번 더 감싸 저장. 조회는 그룹 ID 목록 복호화 → 그룹 키 임포트 → 멤버 식별자 병렬 복호화의 3 단계로 나누고, 단계별로 TanStack Query 독립 캐시 + 복호화 실패 시 재시도 차단을 적용해 무의미한 백엔드 호출을 막음.

Result

서버 로그·DB 어디서도 그룹-멤버 관계를 평문으로 재구성 불가능. 그룹장도 본인 키 없이는 그룹 키를 풀 수 없는 대칭 구조 확보. 복호화 실패가 네트워크 재시도로 전파되지 않아 불필요 트래픽 0 회.

추천 요청 시 사용자 신원과 행동 이력을 분리하는 익명 식별자 생성

HMAC-SHA256TanStack Query프라이버시

Context

장소 추천 모델은 사용자별 행동 이력을 누적해야 추천 품질이 유지됨. 그러나 매 요청마다 실제 사용자 ID 를 그대로 보내면 일정·장소 방문 이력과 실제 신원이 서버에서 결합 가능해짐.

Problem

매번 랜덤 ID 를 새로 만들면 행동 이력이 단절되어 추천 학습이 불가. 반대로 사용자 ID 를 그대로 송신하면 익명성이 깨짐. 같은 사용자가 항상 같은 값을 재현하면서도, 그 값에서 원본 ID 를 역산할 수 없는 식별자가 필요.

Action

로그인 시 브라우저 로컬에 사용자별 인덱스 키를 보관해두고, 추천 요청 직전 HMAC-SHA256 으로 결정성 익명 식별자를 생성해 사용자 ID 대신 송신. HMAC 은 같은 입력에 같은 출력을 보장하므로 동일 사용자의 행동 이력이 일관되게 누적되지만, 단방향 해시이므로 식별자에서 원본 ID 를 역산할 수 없음. 추천 결과는 TanStack Query 캐시에 저장해 장소 후보 목록·투표·확정 흐름과 그대로 합류.

Result

추천 요청에서 원본 사용자 ID 평문 전송 0 회. 동일 사용자가 항상 동일 식별자를 재현해 추천 모델의 사용자별 패턴 학습은 끊김 없이 유지. 신원-이력 결합 가능성을 클라이언트 단계에서 차단.

드래그 입력 상태로 폴링을 제어하는 실시간 시간표 조율

TanStack QueryPointer EventsUX

Context

여러 그룹원이 동시에 가능한 시간대를 드래그로 표시하는 화면이 핵심 UX. 다른 사람의 변경 사항은 빠르게 반영되어야 하지만, WebSocket 서버를 별도로 운영하기엔 인프라 비용과 보안 모델 유지 비용이 모두 부담스러웠음.

Problem

고정 5 초 폴링은 내가 드래그하는 도중 서버 응답이 도착하면 입력을 덮어써 UX 가 깨짐. 창 포커스 기반 갱신만으로는 같은 화면에 머무는 동료의 변경을 제때 반영하지 못함.

Action

TanStack Query 의 refetchInterval 을 입력 모드에 대한 함수로 두고, 시간표 셀의 pointerdown 에서 폴링 OFF, pointerup 에서 ON 으로 전환. 리페치 중 화면이 깜빡이지 않도록 직전 데이터를 placeholderData 로 유지하고, 비활성 탭에서는 폴링을 멈춰 불필요한 네트워크 요청을 차단. 마우스·터치·펜을 Pointer Events 단일 API 로 통합하고 touch-action: none 으로 iOS Safari 스크롤 충돌까지 함께 해결.

Result

WebSocket 인프라 비용 0 원으로 다중 사용자 실시간 조율 UX 확보. 드래그 중 내 입력이 서버 응답에 덮이는 충돌 0 건. 비활성 탭에서의 폴링 0 회로 자원 낭비 제거.

인터랙티브 · 종단간 암호화 단계 비교

각 단계에서 브라우저가 보유한 값과 서버에 도달한 값을 좌우 두 열로 나란히 비교. 상단 탭으로 로그인 · 그룹 생성 · 약속 생성 세 가지 흐름을 전환할 수 있음.

시나리오

비밀번호 입력부터 사용자 키 안전 보관까지

1 / 6

1단계

사용자 입력 — 브라우저 메모리에만 존재

브라우저

Client

  • 사용자 ID민감NEW
    dean@example.com
  • 비밀번호민감NEW
    hunter2-literal
네트워크(이번 단계는 송신 없음)

서버 · DB

Server

  • (수신 없음)

로그인 폼에 입력한 시점. 어떤 네트워크 호출도 일어나지 않았고, ID·비밀번호는 입력 필드 메모리에만 존재한다.

범례민감 (평문)키 (메모리/IDB 전용)해시암호문

더 읽을거리

프로젝트 배경과 추천 모델 — 학회 발표 회고

팀이 어떤 문제 의식에서 출발했는지, 추천 모델이 익명성을 유지한 채 어떻게 학습되는지, 그리고 학부 연구로 진행해 한국정보처리학회 학술발표대회에 게재되기까지의 과정을 정리.