
기능은 정상적으로 동작했지만,
사용자가 실패했을 때 다시 시도할 수 있는지는 따로 고민해야 했다.
아이든든 프로젝트를 진행하면서 가장 많이 느낀 점은
실제 서비스 개발은 단순히 API가 성공하는지만 확인해서는 부족하다는 것이었다.
회원가입, 온보딩, 자녀 등록, 아동수당 연결, 증여 계약, ETF 증여, 배포 환경까지
각 기능은 따로 존재하는 것처럼 보이지만 실제 사용자 흐름에서는 모두 연결되어 있었다.
회원가입
→ 온보딩
→ 자녀 등록
→ 아동수당 연결
→ 증여 계약 생성
→ ETF 투자 관리
→ 목표 달성률 확인
그래서 하나의 기능에서 예외가 발생하면, 단순히 “에러 메시지를 보여준다”로 끝나지 않았다.
사용자가 다시 시도할 수 있는지, 어디서부터 이어갈 수 있는지, 이미 저장된 데이터와 충돌하지 않는지를 함께 고민해야 했다.
이번 글에서는 아이든든 프로젝트를 진행하면서 마주했던 트러블슈팅 중
사용자 흐름, 예약 데이터, 금융성 데이터, 성능 측정과 관련된 이슈들을 정리해보려고 한다.
초기 온보딩 흐름은 부모 계정 생성과 자녀 등록이 하나의 흐름으로 이어져 있었다.
부모 정보 입력
→ 자녀 정보 입력
→ 회원가입 API 호출
→ 자녀 등록 API 호출
겉으로 보기에는 자연스러운 흐름이었다.
부모가 서비스를 이용하려면 자녀 정보가 필요하고,
아이든든은 자녀의 미래 자금을 관리하는 서비스이기 때문에 회원가입 과정에서 자녀 정보까지 받는 것이 당연해 보였다.
하지만 실제 테스트를 하면서 문제가 발생했다.
부모 계정 생성은 성공했는데, 이후 자녀 등록 API에서 실패하는 경우였다.
부모 계정 생성 성공
→ 자녀 등록 실패
→ 사용자가 처음부터 다시 시도
→ 이미 생성된 이메일이라 중복 오류 발생
사용자 입장에서는 회원가입이 실패한 것처럼 보이지만,
서버에는 이미 부모 계정이 생성되어 있었다.
그래서 다시 처음부터 시도하면 이메일 중복 오류가 발생했다.
문제의 핵심은 부모 계정 생성과 자녀 등록을 하나의 필수 흐름처럼 처리했다는 점이었다.
하지만 실제로 두 작업은 성격이 다르다.
| 단계 | 성격 |
|---|---|
| 부모 계정 생성 | 서비스 이용을 위한 필수 단계 |
| 자녀 등록 | 서비스 핵심 기능을 위한 정보 입력 단계 |
| 아동수당 연결 | 선택적 연결 단계 |
부모 계정 생성은 반드시 필요하지만,
자녀 등록은 실패하더라도 나중에 다시 등록할 수 있어야 했다.
즉, 자녀 등록 실패가 회원가입 전체 실패로 이어지면 안 되는 구조였다.
부모 정보 입력
→ 회원가입 성공
→ 자동 로그인
→ 자녀 등록
이 방식의 장점은 부모 계정이 생성된 이후 자녀 등록에 실패하더라도 다시 시도할 수 있다는 점이다.
하지만 단점도 있었다.
자녀가 없는 상태로 앱에 진입할 수 있기 때문에,
앱 전체에서 자녀 미등록 상태를 고려해야 한다.
홈 화면
→ 등록된 자녀 없음
증여 화면
→ 자녀 등록 필요
목표 설정
→ 자녀 등록 후 이용 가능
즉, 프론트와 백엔드 모두 “자녀가 아직 없는 사용자”를 정상 상태로 처리해야 했다.
회원가입 성공
→ 자녀 등록 시도
→ 실패해도 앱 진입
→ 자녀는 나중에 등록
이 방식은 자녀 등록 실패 때문에 회원가입 전체가 막히지 않는다.
회원가입은 성공한 것으로 보고,
자녀 정보는 앱 내부에서 다시 등록할 수 있게 하는 방식이다.
최종적으로는 B안이 더 현실적이라고 판단했다.
자녀 정보 화면에 나중에 하기 버튼을 추가하고,
자녀 등록 실패 시에도 앱 진입이 가능하도록 설계하는 방향이 더 자연스러웠다.
signup 성공
→ 자동 로그인
→ 자녀 등록 시도
→ 성공 또는 실패
→ 다음 단계 진행
→ 자녀는 앱 내에서 추가 가능
이 구조에서는 자녀 등록이 실패하더라도 사용자는 서비스를 완전히 다시 시작하지 않아도 된다.
회원가입 과정에서 모든 정보를 반드시 한 번에 받으려고 하면 실패 복구가 어려워진다.
필수 정보와 나중에 입력해도 되는 정보를 구분해야 한다.
특히 온보딩처럼 여러 단계를 거치는 흐름에서는
각 단계가 실패했을 때 사용자가 어디서부터 다시 시작할 수 있는지 설계하는 것이 중요했다.
온보딩 단계에서 부모 정보, 자녀 정보 등을 입력하던 중 새로고침하면 입력값이 모두 사라지는 문제가 있었다.
사용자는 여러 단계를 거쳐 정보를 입력하고 있었는데,
중간에 새로고침이 발생하면 처음부터 다시 입력해야 했다.
부모 정보 입력
→ 자녀 정보 입력 중
→ 새로고침
→ 입력값 초기화
온보딩 데이터가 React 메모리 상태로만 관리되고 있었기 때문이다.
React state는 페이지가 유지되는 동안에는 값을 보관하지만,
새로고침이 발생하면 초기화된다.
React state
→ 페이지 새로고침 시 초기화
| 방식 | 설명 |
|---|---|
sessionStorage 저장 | 새로고침해도 온보딩 입력값 유지 |
| 서버에 임시 저장 | 단계별 저장 가능하지만 구현 복잡도 증가 |
| 새로고침 시 처음부터 다시 | 구현은 단순하지만 사용자 경험은 아쉬움 |
처음에는 sessionStorage에 온보딩 입력값을 저장하는 방법을 고려했다.
부모 정보 입력
→ sessionStorage 저장
→ 새로고침
→ 저장된 값 복구
이 방식은 사용자 경험 측면에서는 좋다.
하지만 민감한 정보나 계좌 관련 정보가 포함될 수 있기 때문에
브라우저 저장소에 어떤 데이터를 저장할지 신중히 정해야 했다.
MVP에서는 온보딩 중 새로고침 시 처음부터 다시 입력하는 것으로 두되,
이미 로그인 토큰이 있는 경우에는 홈으로 이동하도록 가드 처리하는 방향이 더 단순하다고 판단했다.
새로고침 발생
→ 토큰 있음
→ 이미 가입된 사용자로 판단
→ 홈으로 이동
토큰 없음
→ 온보딩 처음부터 시작
즉, 모든 입력값을 복구하기보다는
현재 서비스 단계에서는 흐름을 단순하게 유지하는 쪽을 선택했다.
온보딩처럼 여러 페이지를 거치는 흐름은 새로고침과 중간 이탈 상황을 반드시 고려해야 한다.
입력값을 유지할지, 다시 시작하게 할지 정책을 명확히 정해야 한다.
특히 브라우저 저장소를 사용할 경우, 편의성과 보안 사이의 균형도 함께 고려해야 했다.
아이든든에는 아동수당을 자녀 계좌와 연결하는 흐름이 있었다.
초기에는 아동수당 “연결”만 고려했다.
하지만 이미 연결된 자녀가 있는 경우,
사용자가 연결을 해지할 수 있어야 하는지도 고민이 생겼다.
아동수당 연결 가능
→ 이미 연결된 상태
→ 해지는 어디서 하지?
초기 설계에는 해지 API나 해지 UI가 없었다.
처음에는 연결 화면에서 해지도 같이 제공하면 되지 않을까 생각했다.
하지만 사용자 흐름을 다시 생각해보니 연결과 해지는 성격이 달랐다.
| 기능 | 성격 |
|---|---|
| 연결 | 온보딩, 혜택 안내, 사용 유도 |
| 해지 | 설정 변경, 신중한 관리 액션 |
연결은 사용자가 서비스를 시작하는 과정에서 자연스럽게 유도할 수 있다.
반면 해지는 이미 연결된 설정을 끊는 행위이므로,
온보딩 화면보다는 마이페이지나 설정 화면에서 관리하는 것이 더 자연스럽다고 판단했다.
아동수당 연결 페이지
→ 연결된 자녀 표시
→ 해지 버튼
→ 확인 모달
→ 해지 API 호출
이 방식은 사용자가 연결 상태를 바로 관리할 수 있다는 장점이 있다.
하지만 연결을 유도하는 페이지 안에 해지 액션까지 있으면 화면 목적이 흐려질 수 있다.
아동수당 연결 페이지
→ 연결만 가능
마이페이지
→ 자녀 정보
→ 아동수당 연결 관리
→ 해지
이 방식은 화면의 역할이 더 명확하다.
연결 페이지는 연결을 돕고,
마이페이지는 이미 연결된 정보를 관리한다.
최종적으로는 해지를 마이페이지에서 처리하는 것이 더 자연스럽다고 판단했다.
다만 백엔드에는 추후 다음과 같은 API가 필요하다고 정리했다.
DELETE /children/{childId}/allowance
또는 상태 변경의 의미를 더 명확히 하려면 다음 방식도 가능하다.
PATCH /children/{childId}/allowance/disconnect
연결과 해지는 같은 기능처럼 보이지만, 사용자 흐름은 다르다.
연결은 온보딩에서 자연스럽지만,
해지는 설정이나 마이페이지에서 관리하는 것이 더 적절할 수 있다.
초기에는 단일 자녀를 기준으로 아동수당 연결 화면을 생각했다.
하지만 실제 서비스에서는 자녀가 여러 명일 수 있다.
첫째
둘째
셋째
이 경우 아동수당을 어떤 자녀에게 연결할지 선택할 수 있어야 했다.
아동수당 연결 화면에서 자녀 목록을 카드 형태로 보여주고,
사용자가 연결할 자녀를 선택할 수 있도록 설계했다.
[선택됨] 자녀 1 카드
[선택됨] 자녀 2 카드
→ 연결하기
기본값은 모든 자녀 선택으로 두었다.
이유는 아동수당 연결 흐름에서 사용자가 특정 자녀만 제외해야 하는 경우보다,
등록된 자녀 전체를 연결하려는 경우가 더 일반적이라고 판단했기 때문이다.
자녀 관련 서비스는 다자녀 케이스를 초기에 고려해야 한다.
단일 자녀 기준으로 설계하면 나중에 UI와 API 흐름을 다시 수정해야 한다.
특히 자녀를 기준으로 계좌, 증여, 목표, 아동수당이 모두 연결되는 서비스에서는
처음부터 다자녀 구조를 고려하는 것이 중요했다.
적립식 증여를 등록하면 예약된 이체가 생성된다.
하지만 화면에서는 완료된 증여 내역만 보여주고 있었다.
const completed = all.filter((t) => t.status === 'COMPLETED');
setGiftTransfers(completed);
이 때문에 적립식 증여를 등록해도
실제 이체가 완료되기 전까지 사용자가 어디에서도 확인할 수 없었다.
적립식 증여 등록 완료
→ 예약된 이체 생성
→ 화면에는 완료 내역만 표시
→ 사용자는 등록이 제대로 되었는지 확인하기 어려움
문제는 증여 내역과 증여 계약을 구분하지 않았다는 점이었다.
GiftTransfer
→ 실제 이체 내역
GiftContract
→ 증여 계약 또는 예약
완료된 이체만 보여주면 사용자는 이미 끝난 내역만 확인할 수 있다.
하지만 금융 서비스에서는 완료된 내역만큼이나
앞으로 발생할 예약 내역도 중요하다.
화면에서 완료된 이체 내역과 진행 중인 증여 계약을 분리해서 보여줄 필요가 있었다.
[완료 내역] [예약/진행 중]
완료된 이체와 예약된 이체를 같은 화면 안에서 탭으로 구분하는 방식이다.
진행 중인 증여 계약
- 매달 용돈
- 매월 15일
- 다음 이체일: 2026-07-15
- 취소 버튼
홈 화면이나 증여 현황 화면에서 진행 중인 계약을 따로 보여주는 방식이다.
내 증여 계약 관리
→ 진행 중
→ 완료
→ 취소됨
계약 단위로 관리할 수 있는 별도 페이지를 두는 방식이다.
이미 백엔드에서는 다음 형태의 API를 사용할 수 있었다.
GET /children/{childId}/gift-contracts
PATCH /gifts/contracts/{contractId}/cancel
따라서 프론트에서는 GiftTransfer만 조회하는 것이 아니라,
GiftContract 목록도 함께 보여주는 구조가 필요했다.
예약/진행 중인 데이터와 완료된 히스토리는 다른 성격의 데이터다.
금융 서비스에서는 사용자가 앞으로 발생할 예약 내역을 반드시 확인할 수 있어야 한다.
특히 적립식 증여처럼 미래에 실행될 계약은
“등록 완료” 이후에도 사용자가 상태를 확인하고 취소할 수 있어야 했다.
ETF 증여에서는 증여 금액을 어떻게 계산할지 고민이 필요했다.
현금 증여는 금액이 명확하다.
현금 500,000원 증여
→ 증여 금액 500,000원
하지만 ETF는 가격이 계속 변한다.
ETF 5주 증여
→ 1주당 가격이 매일 변동
→ 어떤 가격을 기준으로 증여 금액을 계산할까?
ETF 증여는 국세청 상장주식 평가 방식에 맞춰 다음 기준을 따르도록 설계했다.
평가기준일 = 증여일
예상금액 = 증여일 전후 2개월 종가 평균 × 수량
최종금액 = 증여일 이후 2개월 데이터까지 확보된 뒤 확정
다만 증여 직후에는 미래 2개월 데이터가 없다.
따라서 바로 최종금액을 확정할 수 없고,
우선 예상금액만 제공한다.
증여 직후
→ estimatedGiftAmount 표시
증여일 + 2개월 이후
→ finalGiftAmount 확정 가능
현재는 스케줄러가 아니라 lazy 방식으로 구현되어 있다.
사용자가 계약 상세 조회
→ valuationBaseDate + 2개월 <= 오늘인지 확인
→ 확정 가능하면 최종금액 계산
→ DB 저장
즉, 누군가 상세 화면을 열어봐야 최종금액이 계산된다.
이 방식은 구현이 단순하다.
하지만 단점도 있다.
사용자가 상세 화면을 보지 않음
→ finalGiftAmount가 계속 미확정 상태로 남을 수 있음
금융 데이터처럼 확정 시점이 중요한 값은
스케줄러를 통해 자동으로 갱신하는 방식도 고려해야 한다.
예를 들어 매일 새벽 1시에 미확정 ETF 증여 계약을 조회하고,
최종금액 확정이 가능한 계약을 처리할 수 있다.
@Scheduled(cron = "0 0 1 * * *")
public void resolveFinalGiftAmounts() {
...
}
흐름은 다음과 같다.
매일 01:00 스케줄러 실행
→ finalGiftAmount가 null인 ETF 증여 계약 조회
→ valuationBaseDate + 2개월 <= 오늘인지 확인
→ 종가 데이터 조회
→ 최종 증여 금액 계산
→ finalGiftAmount 저장
Lazy 계산은 구현이 단순하지만, 사용자가 조회하지 않으면 데이터가 갱신되지 않는다.
확정 시점이 중요한 금융 데이터는
사용자 조회와 관계없이 스케줄러로 자동 확정하는 방식도 고려해야 했다.
이번 트러블슈팅을 정리하면서 느낀 점은,
실제 서비스 개발은 코드만 잘 짜는 것으로 끝나지 않는다는 것이다.
아이든든은 프론트, 백엔드, MTS, API Gateway, S3, Redis, CloudFront, EC2가 모두 연결된 프로젝트였다.
Frontend
→ API Gateway
→ Core Service
→ ETF Service
→ MTS Service
→ Redis / S3 / RDS
→ CloudFront / EC2
이런 구조에서는 한 곳의 작은 설정 누락이나 흐름 설계 부족이
전체 사용자 경험에 영향을 줄 수 있었다.
프로젝트 후반으로 갈수록 단순 기능 구현보다 중요한 것은
서비스 흐름 전체를 안정적으로 연결하는 것이었다.
회원가입에서 온보딩으로,
온보딩에서 자녀 등록으로,
자녀 등록에서 아동수당과 증여로,
증여에서 ETF 투자와 목표 달성률로 이어지는 흐름이 모두 맞아야
사용자는 서비스를 자연스럽게 사용할 수 있다.
이번 과정에서 가장 많이 배운 것은 다음과 같다.
1. 프론트와 백엔드의 enum, 날짜 포맷, 응답 구조는 반드시 문서화해야 한다.
2. Gateway 라우팅은 prefix 순서와 누락 여부를 계속 확인해야 한다.
3. Redis, S3, CloudFront 같은 외부 인프라는 timeout, CORS, 권한 설정이 중요하다.
4. 배포 환경에서는 GitHub Secret이 실제 설정의 기준이 된다.
5. 금융 서비스 화면에서는 확정값과 예정값을 반드시 구분해야 한다.
6. 민감 정보는 절대 코드나 문서에 남기면 안 된다.
앞으로는 기능을 구현할 때마다 다음 질문을 먼저 확인해야겠다고 느꼈다.
이 요청은 실제 배포 환경에서도 같은 경로로 가는가?
프론트와 백엔드가 같은 enum과 날짜 포맷을 쓰고 있는가?
이 데이터는 확정값인가, 예정값인가?
이 설정은 서버에만 있는가, GitHub Secret에도 반영되어 있는가?
사용자가 실패했을 때 다시 시도할 수 있는가?
결국 서비스의 완성도는
성공 케이스를 얼마나 잘 처리하는지뿐만 아니라,
실패했을 때 얼마나 자연스럽게 복구할 수 있는지에서 결정된다고 느꼈다.