팀 구성: 프론트엔드 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, 원자적 쓰기 등을 통해 비용과 성능의 균형점을 찾아 서비스 안정성을 높였습니다.