
팀 구성: 서비스 기획 1인, UI/UX 디자이너 2인, 프론트엔드 3인, 백엔드 2인 (현대자동차 소프티어 7기)
역할: 프론트엔드 개발 (실시간 첨삭, 실시간 알림 파이프라인 구축, 자료 업로드 및 보안 로직 구현)
주요 기술 스택: React, TypeScript, Vite, Tailwind CSS, TanStack Query, WebSocket, SSE
개요: 취준생들이 흩어진 경험을 모으고 자기소개서를 효율적으로 작성/관리하며 실시간 첨삭을 지원하는 웹서비스입니다.
주요 경험: WebSocket 및 SSE 통신 최적화와 인메모리 보안 로직 등 5가지 주요 기술적 과제를 해결하여 서비스 성능과 안정성을 확보했습니다.
문제 상황: 자기소개서 작성자(Writer)와 첨삭자(Reviewer)가 동시에 자기소개서를 접근할 때 웹소켓 이벤트 수신과 프론트엔드 상태 관리가 동기화되지 않아 텍스트 버전이 꼬이거나 UI가 즉각적으로 갱신되지 않았습니다.
해결 전략: STOMP/SockJS 연결 생명주기를 훅으로 분리해 안정적으로 관리하고 이벤트 수신 시 TanStack Query 캐시를 직접 조작해 실시간으로 강제 동기화하기로 했습니다.
문제 해결 과정: useStompClient 훅을 설계하여 첨삭 모드 활성화 시에만 웹소켓을 제어하도록 최적화했습니다. 서버로부터 업데이트(TEXT_UPDATE 등) 이벤트를 수신하면 useSocketMessage 핸들러에서 invalidateQueries를 호출해 즉시 UI를 갱신하고 버전 대체 이벤트(TEXT_REPLACE_ALL) 수신 시 프론트 캐시 텍스트와 버전을 직접 맞춰 충돌을 방지했습니다.
최종 결과: 동시 접근 환경에서도 버전 꼬임 없이 안정적이고 매끄러운 실시간 동시 편집 및 첨삭 기능을 제공했습니다.
문제 상황: 실시간 알림을 위해 SSE를 도입했으나 사용자가 브라우저 다중 탭을 열 경우 각 탭마다 독립적인 연결이 생성되어 서버 리소스가 낭비되고 유저당 최대 연결 수를 초과하는 문제가 있었습니다.
해결 전략: 여러 탭이 단일 SSE 연결을 공유하도록 클라이언트 아키텍처를 개선하고 재연결 시 발생하는 서버 부하 트래픽을 분산시키는 전략을 도입했습니다.
문제 해결 과정: 브라우저의 Shared Worker를 활용해 백그라운드에서 하나의 SSE 연결만 유지하고 활성화된 모든 탭이 이 워커를 통해 메시지를 공유받도록 다중 탭 환경을 최적화했습니다. 또한, 네트워크 끊김 등으로 인한 재연결 시 지수 백오프(Exponential Backoff)와 Jitter 알고리즘을 적용하여 클라이언트의 요청이 일시에 몰리는 현상을 방지했습니다.
최종 결과: 불필요한 중복 커넥션을 원천 차단하여 서버 리소스를 크게 절감하고 재연결 트래픽을 효과적으로 분산시켜 실시간 알림 파이프라인을 구축했습니다.
문제 상황: Access Token을 로컬 스토리지에 보관하면 XSS 공격에 취약해지고 보안을 위해 메모리에만 보관하면 새로고침 시 로그아웃되는 UX 저하 문제가 있었습니다.
해결 전략: 보안과 UX를 동시에 챙기기 위해 토큰을 메모리에 저장하면서도 백그라운드에서 토큰을 자동 갱신하는 Silent Refresh 방식을 도입했습니다.
문제 해결 과정: Access Token을 JS 변수(인메모리)에 저장하여 외부 접근을 차단했습니다. 토큰 만료 시 HttpOnly 쿠키에 담긴 Refresh Token을 통해 사용자 모르게 백그라운드 API를 호출해 Access Token을 재발급하는 로직을 구현했습니다.
최종 결과: 강력한 보안 환경(XSS 원천 차단)을 유지하면서도 사용자는 새로고침 시에도 끊김 없는 세션을 경험할 수 있도록 시스템을 고도화했습니다.
문제 상황: AI가 업로드된 PDF를 파싱하는 긴 시간 동안 사용자가 로딩 화면을 하염없이 기다려야 했으며, 이 대기 시간 중 사용자의 추가적인 화면 조작으로 인해 예상치 못한 버그가 발생할 위험이 있었습니다.
해결 전략: 파싱 과정을 백그라운드 비동기 처리로 전환하여 대기 화면을 없애고 작업이 완료되면 실시간 알림으로 안내해 사용자 경험을 개선하기로 했습니다.
문제 해결 과정: S3에 파일 업로드 즉시 성공 응답을 받고 다른 화면으로 자유롭게 이동할 수 있도록 프론트엔드 흐름을 전면 수정했습니다. 이후 AI 파싱이 완료되면 구축해둔 SSE 실시간 알림을 통해 사용자에게 결과를 띄워주도록 로직을 연동했습니다.
최종 결과: 불필요한 로딩 화면을 제거하여 UX를 크게 향상시켰으며, 로딩 중 이탈이나 중복 클릭 등으로 발생할 수 있는 잠재적 프론트엔드 버그를 사전에 완벽히 차단했습니다.
문제 상황: 알림 보관 기한이 7일이라 데이터가 적을 것으로 예상했으나 첨삭 코멘트마다 알림이 생성되어 무한 스크롤 시 뷰포트에 렌더링되는 DOM 요소가 과도하게 누적되는 성능 저하 이슈가 발생했습니다.
해결 전략: 화면에 보이지 않는 수많은 알림 요소까지 모두 렌더링하는 대신 실제 뷰포트에 보이는 요소만 DOM에 그리는 리스트 가상화(List Virtualization)를 도입하기로 했습니다.
문제 해결 과정: 알림 리스트에 가상화 기법을 적용하여 스크롤 위치에 따라 화면에 보이는 인덱스의 요소들만 동적으로 렌더링하고 벗어난 요소는 DOM에서 제거되도록 컴포넌트를 최적화했습니다.
최종 결과: 알림이 수백 개 이상 쌓여도 DOM 노드 수가 일정하게 유지되어 스크롤 버벅거림 없이 부드럽고 쾌적한 UI 성능을 확보했습니다.
팀 구성: 프론트엔드 1인, 백엔드 1인
역할: 기획, 디자인, 프론트엔드 개발 (1인)
주요 기술 스택: React, TypeScript, Tailwind CSS
개요: IT 초심자가 공모전, 프로젝트, 스터디 팀원을 모집하는 웹 서비스입니다.
주요 경험: 4가지 주요 문제 해결 경험을 통해 서비스 완성도를 높였습니다.
문제 상황: 팀원 모집 필터링에 여러 드롭다운이 존재했습니다. 초기 구현 시 Props Drilling으로 상태를 전달해 코드가 복잡했고 여러 드롭다운이 동시에 열리는 UI 오류가 발생했습니다.
해결 전략: UX 측면에서 한 번에 하나의 드롭다운만 활성화되도록 결정했습니다. 개별 상태를 전역 상태로 통합 관리하여 Props Drilling과 UI 오류를 동시에 해결하고자 했습니다.
문제 해결 과정: 모든 드롭다운의 ‘열림’ 상태를 하나의 전역 상태로 통합했습니다. 특정 드롭다운 클릭 시 해당 식별값을 전역 상태에 저장하고 나머지는 ‘닫힘’ 처리했습니다.
최종 결과: Prop Drilling을 해결하고 코드 복잡도를 개선했습니다. UI 오류를 수정하여 UX를 향상시켰습니다.
배운 점: 전역 상태 관리의 필요성을 체감했으며 추후 Recoil 등 전용 라이브러리 도입을 계획하게 되었습니다.
문제 상황: 로그인 등 이벤트 완료 후 페이지가 이동/새로고침되면 React 컴포넌트 재렌더링으로 토스트 메시지가 즉시 사라져 사용자가 피드백을 인지할 수 없었습니다.
해결 전략: 1안(setTimeout)은 ‘서비스가 느리다’는 인상을 줄 수 있어 폐기했습니다. 2안으로 메시지를 localStorage에 임시 저장하고 페이지 로드 후 불러와 보여주는 전략을 채택했습니다.
문제 해결 과정: 이벤트 성공 시 메시지를 localStorage에 저장하고 페이지를 이동시켰습니다. 페이지 로드 시 useEffect 훅에서 localStorage를 확인하여 메시지가 있으면 토스트를 띄우고 즉시 삭제했습니다.
최종 결과: 페이지가 이동되어도 사용자 피드백(토스트)이 유실되지 않도록 보장하여 서비스의 완성도와 사용자 신뢰도를 높였습니다.
문제 상황: 초기 기획 시 PC 환경만 고려하여, 모바일에서 레이아웃이 깨지고 사용이 불가능한 상태였습니다.
해결 전략: 1안(모바일 접근 제한)은 회원 모집 목표에 치명적이라 폐기했습니다. 2안으로 모든 기기에서 일관된 경험을 제공하고 트래픽을 확보하기 위해 반응형 웹을 적용하기로 했습니다.
문제 해결 과정: Tailwind CSS의 반응형 분기점(sm, md 등) 유틸리티를 활용했습니다. 주요 컴포넌트(헤더, 카드 리스트)를 모바일 뷰 기준으로 재설계하고 flex-col, md:flex-row처럼 화면 크기에 따라 레이아웃이 변경되도록 CSS 클래스를 전면 수정했습니다.
최종 결과: 모바일, 태블릿, PC 등 모든 디바이스 크기에서 최적화된 UI/UX를 제공하는 반응형 웹을 구현했습니다.
배운 점: 프로젝트 초기 기획 단계부터 반응형 디자인을 고려해야 개발 비용을 최소화할 수 있음을 깨달았습니다.
문제 상황: 네트워크가 불안정할 경우, 버튼 클릭에 대한 시각적 피드백이 즉각적이지 않았습니다. 이로 인해 사용자가 서비스를 멈췄다고 오해하거나 버튼을 중복 클릭하여 불필요한 API 호출을 발생시켰습니다.
해결 전략: API 통신이 ‘진행 중’임을 알리는 로딩 스피너를 도입하고 통신 중에는 버튼을 비활성화하여 중복 요청을 원천적으로 차단하기로 했습니다.
문제 해결 과정: API 요청 상태(isLoading)를 useState로 관리했습니다. isLoading이 true일 때 BeatLoader를 표시하고, 버튼의 disabled 속성을 활성화했습니다. 요청이 완료되면 isLoading을 false로 변경해 버튼을 원상 복구했습니다.
최종 결과: API 요청 중임을 시각적으로 명확히 보여주어 UX를 향상시켰습니다. 또한, 버튼 비활성화를 통해 불필요한 중복 API 호출을 방지하여 서비스 안정성을 높였습니다.

팀 구성: 안드로이드 2인
역할: 기획, 디자인, 안드로이드 개발, Firebase 통신
주요 기술 스택: Kotlin, Jetpack Compose, Room, Hilt, Firebase
개요: 외국인과 한국인이 여행을 통해 언어 교환을 할 수 있는 안드로이드 앱 서비스입니다.
주요 경험: 8가지 주요 기술적 의사결정 및 문제 해결을 통해 앱의 성능과 안정성을 확보했습니다.
문제 상황: 드롭다운 등 즉각적 반응이 필요한 UI가 공공데이터 API(예: 시군구 목록)의 응답 속도에 의존하여 사용자 경험(UX)을 심각하게 저해했습니다.
해결 전략: 변경 빈도가 낮고 용량이 작은 텍스트 데이터(시군구 목록)는 앱 최초 실행 시 로컬 DB에 캐싱(Caching)하여 API 통신 없이 즉각적으로 불러오기로 결정. 이를 위해 Room을 도입했습니다.
문제 해결 과정: 앱 로그인 시점에 공공데이터 API를 1회 호출하여 시군구 데이터를 가져온 뒤 Room DB에 저장했습니다. 이후 관련 드롭다운은 API가 아닌 Room의 DAO를 통해 데이터를 조회하도록 로직을 수정했습니다.
최종 결과: API 네트워크 응답 시간(평균 1~2초)을 제거하고, 로컬 DB에서 데이터를 즉각(0.1초 미만) 로드하여 UX를 대폭 향상시켰습니다.
문제 상황: 개발 중 Entity/DAO 수정(필드 추가/삭제 등) 후 앱을 실행하면 Room DB에 접근 시 앱이 비정상 종료되거나 UI 오류가 발생하는 문제가 반복되었습니다.
해결 전략: 원인 분석 결과, Room이 스키마의 해시값을 관리하며 스키마 변경 시 version을 올리지 않으면 충돌이 발생함을 확인했습니다. 또한, 잦은 스키마 변경이 일어나는 개발 단계에서는 강제 마이그레이션이 필요하다고 판단했습니다.
문제 해결 과정: Entity 수정 시 build.gradle의 Room version을 1씩 증가시켰습니다. Room.databaseBuilder에 .addMigrations(...)를 추가하여 마이그레이션 규칙을 정의하여 해결했습니다.
최종 결과: 스키마 변경 후에도 앱이 충돌 없이 안정적으로 실행되었습니다.
배운 점: 추후 개발 초기 단계에는 스키마 충돌을 원천 차단하는 .fallbackToDestructiveMigration() 옵션을 적용하여 개발 효율성을 높일 계획입니다.
문제 상황: 커스텀 객체(List)나 해시맵(Map)을 Entity의 필드로 직접 저장하려 할 때 Room이 해당 타입을 인식하지 못해 컴파일 오류가 발생했습니다.
해결 전략: Room이 알 수 없는 객체 타입을 DB에 저장 가능한 기본 타입(예: String)으로 변환해 주는 @TypeConverter를 도입하기로 했습니다.
문제 해결 과정: List나 Map을 JSON 형태의 String으로 변환하는 함수와 String을 다시 List나 Map으로 변환하는 함수를 가진 TypeConverter 클래스를 작성하고 @Database 어노테이션에 등록했습니다.
최종 결과: 오류를 해결하고 커스텀 객체 및 리스트 등을 Room DB에 성공적으로 저장/조회할 수 있게 되어 데이터 모델의 유연성을 확보했습니다.
문제 상황: 초기 개발 시 ViewModel이나 Repository가 필요한 객체(Room DAO, Firebase 인스턴스 등)를 직접 생성했습니다. 이로 인해 클래스 간 의존성이 높아져 코드 재사용성이 떨어졌습니다.
해결 전략: 안드로이드 공식 DI 프레임워크인 Hilt를 도입하여 객체 생성의 책임을 외부(Hilt)에 위임하고 필요한 곳에서는 주입만 받도록(DI) 구조를 변경하기로 했습니다.
문제 해결 과정: Database, ViewModel 등에 Hilt 어노테이션을 적용했습니다. Room, Firebase 등 공통 모듈은 @Module, @Provides로 정의하고, ViewModel의 생성자에는 @Inject를 사용해 Repository를 주입받도록 리팩토링했습니다.
최종 결과: 객체 생성 로직이 분리되어 코드의 결합도가 낮아졌습니다. ViewModel이 특정 구현체에 의존하지 않게 되어 코드의 확장성과 유지보수성이 향상되었습니다.
문제 상황: Jetpack Compose Composable 함수 내에서 API 통신, 데이터 가공 등 비즈니스 로직을 함께 처리했습니다. 이로 인해 Composable이 비대해지고 UI 로직과 비즈니스 로직이 강하게 결합되어 코드 파악 및 수정이 어려웠습니다.
해결 전략: Google이 권장하는 MVVM 아키텍처 패턴을 도입하여 역할을 명확히 분리하기로 했습니다. UI(View)는 상태 표시와 이벤트 전달, ViewModel은 비즈니스 로직 처리와 상태 관리, Model(Repository)은 데이터 소스 접근을 담당하도록 했습니다.
문제 해결 과정: Composable 함수 내의 모든 비즈니스 로직을 ViewModel로 이전했습니다. ViewModel은 StateFlow를 통해 UI 상태(State)를 노출하고 Composable은 이 상태를 collectAsState()로 구독하여 화면을 그리도록 변경했습니다.
최종 결과: 관심사의 분리(SoC)를 통해 코드의 가독성과 유지보수성이 대폭 향상되었습니다. 특히 ViewModel이 UI 프레임워크(Compose)에 의존하지 않게 되어 로직의 재사용성과 테스트 용이성이 높아졌습니다.
문제 상황: 여행 목록 → 여행 상세 화면으로 이동 시 데이터 객체 전체를 전달하여 속도를 높일지 ID만 전달하여 데이터 최신화를 보장할지 결정해야 했습니다.
해결 전략: 서비스 특성을 고려 (1) 자주 변경되는 데이터를 사용해야 할 때는 ID만 전달하여 최신 상태를 보장하고 (2) 변경되지 않는 정적 데이터는 객체 전체를 전달하여 API 호출 비용을 줄이기로 했습니다.
문제 해결 과정: (1) ‘알림 기능’: notificationId만 전달하여 알림에 보여줄 상대방 프로필 정보를 실시간 구독(fetch)했습니다. (2) ‘여행 지원서 기능’ → ‘프로필 상세 정보’: 지원 시점의 정보가 중요한 applicant 객체 전체를 모집글에서 1회의 API 호출로 즉시 UI를 그렸습니다.
최종 결과: 알림에서는 데이터 정합성을 확보하고 정적 상세 페이지에서는 불필요한 API 호출을 줄여 로딩 속도를 향상시키고 기능을 의도대로 제작하는 등 각 화면의 특성에 맞는 최적화된 데이터 전달 방식을 적용했습니다.
문제 상황: 메모리 부족 등으로 안드로이드 시스템이 앱 프로세스를 강제 종료했다가 다시 돌아올 경우 Navigation Argument로 전달받은 ID 등 핵심 데이터가 유실되어 앱이 비정상 동작했습니다.
해결 전략: 기존 NavBackStackEntry의 arguments 방식은 프로세스 종료 시 데이터를 보장하지 못합니다. 따라서 프로세스 종료에도 안전한 SavedStateHandle을 통해 Navigation Argument를 수신하도록 Hilt ViewModel을 구성하기로 결정했습니다.
문제 해결 과정: Hilt ViewModel의 생성자에 SavedStateHandle을 추가했습니다. ViewModel 내에서는 savedStateHandle.get<String>("postId")와 같은 방식으로 Argument를 안전하게 가져오도록 수정했습니다.
최종 결과: 시스템(OS)에 의해 앱 프로세스가 강제 종료된 후 복귀하는 ‘Process Death’ 상황에서도 SavedStateHandle이 postId를 복원해 주어, 앱이 중단된 지점부터 안정적으로 로직을 재개할 수 있게 되었습니다.
문제 상황: 기능 구현을 위해 Firebase(Realtime Database/Firestore)를 사용함에 따라 데이터 Read/Write 횟수가 증가하여 잠재적인 비용 문제가 예상되었습니다.
해결 전략: 무분별한 실시간 구독을 지양하고 N+1 쿼리를 방지하며, 쓰기 작업을 원자적으로 처리하는 세 가지 전략을 수립하여 비용을 최적화했습니다.
문제 해결 과정: (1) 수동 갱신 도입: 자주 바뀌지 않는 데이터는 get()을 이용한 단발성 fetch로 처리하여 불필요한 Read 비용을 절감했습니다.
(2) 멀티패스 업데이트: 지원서 제출 시, 모집글, 알림 등에 각각 업데이트하지 않고 멀티패스 업데이트를 하기 위해 단일 원자적 쓰기(1회)로 처리했습니다. (통신 중 네트워크 문제가 발생해도 데이터 정합성 확보 및 쓰기 비용 1/N 절감)
(3) N+1 쿼리 방지: 사용자가 누른 ‘관심’ 목록을 가져올 때 발생하는 N+1 쿼리 문제를 해결하기 위해 비즈니스 로직(기획)을 수정하여 ‘관심’ 개수를 5개로 제한하여 기회 비용에 대한 재미 요소를 추가했습니다.
최종 결과: 비즈니스 로직(기획) 수정을 통해 Firebase 비용을 최소화하고 단발성 fetch, 원자적 쓰기 등을 통해 비용과 성능의 균형점을 찾아 서비스 안정성을 높였습니다.