오늘은 처음에는 단순히 프론트가 백엔드랑 잘 붙었나를 확인하는 일처럼 시작했다. 그런데 막상 화면을 켜고 하나씩 눌러 보니, 문제는 API가 붙었는지 안 붙었는지보다 훨씬 더 넓었다. 데이터는 오고 있었고, 서버도 어느 정도는 살아 있었지만, 사용자가 실제로 보는 경험은 아직 별친구답지 않았다.
특히 오늘 계속 느낀 건 기능이 존재한다와 경험으로 느껴진다는 완전히 다르다는 점이었다. 백엔드에 경험치가 있고, 캐릭터 성장 단계가 있고, AI 대화가 있고, 기억 조각이 있어도 프론트에서 그걸 적절한 위치와 크기, 말투, 타이밍으로 보여주지 않으면 사용자는 그냥 어색하다고 느낀다. 오늘 작업은 그 어색함을 하나씩 잡는 과정이었다.
프론트에서 본 문제들
먼저 홈 화면에서 별친구가 너무 크게 보였다. 레벨 1 무무가 레벨 3 캐릭터와 비교해도 과하게 커 보였고, 경험치나 상태 UI가 캐릭터를 가리는 느낌도 있었다. 그래서 캐릭터 자체를 보여주는 영역과 정보 뱃지를 분리해서 생각해야 했다. 캐릭터는 캐릭터답게 보이고, 정보는 짧고 작게 옆이나 아래로 붙어야 한다.
경험치도 처음에는 거의 보이지 않았다. 백엔드에서는 성장 정보가 있는데 홈에서 체감되지 않으니, 사용자는 "내가 미션을 해서 뭐가 달라졌지?"를 알 수 없었다. 그래서 홈 응답에 성장 정보를 연결하고, 프론트에서는 레벨/경험치/다음 성장까지 남은 수치를 짧게 보여주는 방향으로 잡았다. 다만 이 수치도 너무 크면 캐릭터를 가리기 때문에, 성장 정보는 보조 정보로 눌러 앉히는 게 맞았다.
대화하기 UX도 크게 손봤다. 처음에는 별친구 상세 화면 아래쪽에 "별친구에게 말 걸기"가 있어서, 메인 기능인데도 스크롤해야 보였다. 별친구 앱에서 대화는 부가기능이 아니라 핵심 기능이다. 그래서 홈에서도 캐릭터 근처에 작게 진입점을 두고, 누르면 별도 대화 페이지로 이동하는 방식이 더 자연스럽다고 판단했다. 기존의 작은 카드 안에 억지로 대화를 넣기보다, talk bg 같은 전용 에셋을 살려서 대화 페이지 자체를 만드는 쪽이 경험상 맞았다.
대화창에서는 또 다른 문제가 있었다. 스트리밍을 붙여 놨는데 화면에서는 첫 단어가 실시간으로 나오는 느낌이 없었다. 그리고 대화가 길어지면 대화창 내부에서 스크롤되는 게 아니라 페이지 전체가 밀리는 문제가 있었다. 채팅 UI라면 메시지 영역은 자기 안에서 스크롤되어야 하고, 입력창은 안정적으로 남아 있어야 한다. 프론트에서는 이 부분을 별도 대화 카드와 로그 영역으로 정리했다.
또 하나 기억에 남는 건 스트리밍 완료 처리였다. 서버에서 done이 왔는데도 프론트가 타이핑 연출을 끝까지 기다리면서 입력창이 오래 잠기는 문제가 있었다. 스트리밍 중에는 첫 토큰부터 보여주는 게 중요하지만, 서버가 완료를 알렸다면 프론트는 더 이상 질질 끌면 안 된다. 그래서 done 이후에는 완성 문장으로 바로 고정하고 입력을 풀도록 바꿨다. 연출보다 사용감이 먼저라는 걸 다시 느꼈다.
미션 결과 화면도 많이 봤다. 경험치 획득 UI는 레벨업이 아닐 때는 작고 간단해야 한다. 반대로 레벨업은 크게 축하받는 느낌이 필요하다. 처음에는 보상 획득 UI와 경험치 UI가 서로 부딪히면서 이상해졌고, 별조각 "보유 n개" 같은 정보도 배치가 어색했다. 보상은 보상대로, 경험치는 경험치대로 위계를 나눠야 했다.
보관함과 상점에서는 스킨 잠금 UX가 중요했다. 레벨 1이나 2에서는 스킨을 살 수도 없고 낄 수도 없는데, 프론트에서 이걸 막지 않으면 사용자는 눌렀다가 실패하는 경험만 하게 된다. 그래서 레벨 3이 되면 살 수 있고 착용할 수 있다는 안내를 붙이고, 잠금 상태에서는 구매/착용을 막는 쪽으로 정리했다.
에셋도 그냥 존재한다고 끝이 아니었다. 캐릭터, 배경, 보상, 배지, 공유 카드용 에셋이 이미 만들어져 있었는데 실제 UI에서 덜 쓰인 것들이 있었다. 그리고 이미지가 드래그되거나 저장되는 느낌도 막아야 했다. 앱 안의 캐릭터 에셋은 그냥 웹 이미지가 아니라 서비스 경험의 일부라서, 드래그 방지 같은 작은 디테일도 필요했다.
날씨 배지는 백엔드 API를 새로 만들지 않고 프론트에서 우선 해결하는 방향으로 잡았다. 예를 들어 울산 날씨가 맑음이면 "햇살" 계열 배지로 보이는 식이다. 다만 같은 의미의 문구가 두 번 나오거나 "햇살 배지"라고 굳이 텍스트로 설명하는 건 UI적으로 어색했다. 배지는 말로 설명하기보다 이미지와 짧은 상태감으로 전달하는 게 낫다.
온보딩/마이페이지 쪽에서는 개인화라는 말이 너무 서비스 내부 용어처럼 느껴졌다. 사용자는 개인화라는 단어보다 "미션 취향"이 더 바로 이해된다. 그래서 화면 문구도 개발자 관점이 아니라 사용자 관점으로 줄이는 게 맞았다.
백엔드에서 본 문제들
백엔드 쪽에서 제일 크게 걸린 건 AI 말투였다. 무무를 사용자가 무다리라고 이름 붙였는데 응답이 "무무가 ~라고 하네요", "사용자가 ~했다고 하네요"처럼 나오는 건 정말 이상했다. 사용자는 캐릭터와 말하고 싶은데, 중간에 해설자가 끼어드는 느낌이었다.
그래서 프롬프트를 다시 봤다. 문제는 안전하게 만들려고 너무 많이 묶어 둔 점이었다. 안전한 말투를 강제하려다 보니 대화가 자꾸 같은 방향으로 닫혔고, 실제 친구처럼 관심을 갖고 되묻는 느낌이 약했다. 오늘 정리한 방향은 이렇다.
- 대화는 미션 비서가 아니라 일상 대화 메이트처럼 받아준다.
- 사용자가 힘들다고 하면 먼저 감정을 받고, 바로 미션이나 해결책으로 끌고 가지 않는다.
- 사용자가 좋은 일을 말하면 같이 기뻐한다.
- 후속 질문은 많아도 하나만 한다.
- 캐릭터 이름은 사용자의 이름이 아니라 별친구 자신의 이름이다.
- "무다리야", "무다리 칭찬해" 같은 말은 무다리가 자기에게 온 말로 받아 1인칭으로 답해야 한다.
무무 말투도 다시 잡았다. 처음에는 "무... 무무. (해석: ...)" 형식만 반복되는 느낌이 강했다. 그런데 무무는 말 자체가 제한된 캐릭터라서, 오히려 괄호 밖의 "무" 리듬이 감정 표현을 담당해야 한다. 좋은 일이면 무! 무무!, 속상하면 무무 ㅠㅠ, 조심스러우면 무우...?처럼 달라져야 한다. 그래서 프롬프트와 fallback 모두 감정별 리듬을 나누고, 검증 정책에서도 괄호 밖 ㅠ를 허용했다. 대신 한국어 의미 문장은 여전히 (해석: ...) 안에만 들어가도록 막았다.
fallback도 중요했다. 처음에는 provider가 정상인지 아닌지 헷갈렸는데, 실제로는 gateway deadline 때문에 AI 응답이 늦으면 fallback으로 떨어지는 경우가 있었다. 이때 fallback 문구가 이상하면 사용자는 AI 전체가 이상하다고 느낀다. 그래서 gateway fallback, AI fallback 모두 간접화법을 빼고 캐릭터답게 바꿨다.
SSE도 다시 봤다. AI 서버가 죽은 게 아니라 gateway의 AI gRPC deadline이 8초라 Gemini 응답이 늦으면 DEADLINE_EXCEEDED로 fallback이 내려왔다. 별친구 대화는 SSE로 기다릴 수 있는 화면이므로, 너무 짧은 deadline은 오히려 provider 응답을 못 보게 만든다. 그래서 gateway 별친구 대화 AI deadline 기본값을 20초로 늘리고, SSE timeout도 45초로 늘렸다. .env 파일 자체를 수정한 건 아니고, application.yaml의 기본값을 바꿨다.
미션 생성도 비슷했다. 원래는 fallback 템플릿을 안전 seed로 두고 AI가 자율적으로 변주해야 하는데, 실제로는 너무 템플릿처럼 반복되는 느낌이 강했다. 프롬프트가 "안전하게"를 너무 강하게 잡으면 AI가 같은 행동만 조금 바꿔서 내놓는다. 그래서 fallback은 비상용 seed일 뿐이고, 시간대/날씨/온보딩/최근 거절 이력과 충돌하지 않는 범위에서는 category와 행동 방식도 바꿀 수 있게 풀었다.
그 대신 아무렇게나 바꾸면 안 되기 때문에 guard와 diversity 쪽도 손봤다. 오늘 이미 나온 actionFamily는 피하고, 물 마시기/책상 정리/목 스트레칭 같은 행동군이 반복되지 않게 classifier를 보강했다. 특히 "손목"이나 "발목"에 들어 있는 "목" 때문에 목/어깨 스트레칭으로 잘못 분류될 수 있는 부분도 신경 썼다. 작은 키워드 하나가 추천 다양성 전체를 망칠 수 있다는 걸 느꼈다.
AI 미션 생성 요청에는 캐릭터 이름도 같이 보내도록 proto와 request를 수정했다. 말투를 만들 때 캐릭터 타입만 알면 충분해 보이지만, 실제로는 사용자가 붙인 이름이 경험의 핵심이다. "무무"가 아니라 "무다리"라고 부르면, 백엔드도 그 이름을 알고 있어야 한다.
캐릭터 터치 반응도 다시 봤다. 기억 조각이 해금되는 느낌은 단순히 한 번 정해진 문구를 계속 보여주는 게 아니라, 계속 터치하다 보면 공통 반응과 이미 본 기억, 새 기억이 랜덤하게 섞여야 살아난다. 그래서 새로 해금 가능한 기억은 랜덤으로 고르고, 없을 때도 공통 반응이나 이미 해금된 기억을 랜덤으로 보여주는 방식으로 바꿨다. 그리고 무무 기본 이름으로 박힌 문구는 사용자 지정 이름으로 치환하도록 처리했다.
홈 API에는 캐릭터 성장 정보를 포함하도록 했다. 프론트에서 경험치 UI를 만들려면 결국 홈 응답에 growth가 있어야 한다. 화면에서 "왜 경험치가 안 보이지?"라고 느낀 문제의 일부는 프론트 배치 문제였지만, 백엔드 응답 설계도 같이 맞춰야 했다.
오늘 제일 많이 배운 것
오늘 제일 크게 느낀 건 "매핑"이라는 단어가 생각보다 넓다는 점이다. 단순히 API 필드를 프론트 타입에 연결하는 게 매핑이 아니었다. 백엔드의 의도, DB 상태, fallback 정책, 프롬프트 문장, 에셋 이름, 화면 위계, 버튼 위치, 애니메이션 타이밍까지 전부 맞아야 사용자가 하나의 경험으로 받아들인다.
그리고 fallback은 정말 조심해야 한다. fallback은 장애 상황에서만 보이는 보조 장치라고 생각하기 쉽지만, 로컬 개발 중에는 deadline, DB 상태, Redis 카운트, provider 지연 때문에 자주 보일 수 있다. 그래서 fallback 문구가 이상하면 "AI가 이상하다"로 느껴진다. fallback도 캐릭터 경험의 일부로 다듬어야 한다.
프롬프트도 너무 빡세게 잡으면 답변이 안전해지는 대신 생기가 죽는다. 특히 캐릭터 대화는 어느 정도 자율성이 있어야 한다. 하지 말아야 할 것들은 분명히 막되, 감정 반응과 질문 방식은 너무 좁게 묶으면 안 된다. 오늘 무무가 계속 무... 무무만 하던 문제도 결국 "형식을 지키게 하려다 리듬까지 죽인" 문제였다.
프론트에서는 크기와 위치가 정말 중요했다. 같은 정보라도 캐릭터를 가리면 방해가 되고, 너무 아래에 있으면 없는 기능처럼 느껴진다. 대화하기는 별친구 서비스의 중심 기능이므로 캐릭터 근처에서 바로 들어갈 수 있어야 했다. 경험치와 배지는 짧게 보이고, 레벨업 같은 큰 순간만 크게 보여주는 게 맞다.
또 하나는 DB/Redis 상태가 테스트 판단을 흐린다는 점이다. "오늘 대화 마감"이 뜨면 프롬프트가 이상한 건지, provider가 fallback인지, Redis daily limit 때문인지 구분해야 한다. 오늘은 Redis daily key를 지우고, AI 대화 세션/메시지를 truncate하고, provider 응답이 fallback_used=false로 저장되는지까지 확인했다. 화면만 보고 판단하면 쉽게 헷갈린다.
마이그레이션도 조심해야 한다. 작업 중에 새 migration 버전을 막 추가하는 방식은 PR 안에서 오히려 지저분해진다. 운영에 이미 올라간 게 아니라면 기존 변경에 반영하거나, 정말 필요한 경우에만 새 migration을 추가해야 한다. 오늘 이 부분은 특히 기억해둬야 한다.
오늘 확인한 것들
- 프론트는 tsc --noEmit으로 타입 확인을 했다.
- AI 쪽은 대화/프롬프트/무무 검증 관련 테스트를 돌렸다.
- AI 서버와 gateway를 재기동하고 실제 대화 provider 응답을 확인했다.
- provider 응답이 DB에 fallback_used=false로 저장되는 것을 확인했다.
- gateway/AI deadline 때문에 fallback으로 떨어지는 케이스를 로그로 확인했다.
- .env 파일 자체는 수정하지 않았고, 설정 기본값은 application.yaml 쪽에서 조정했다.
다음에 보면 좋을 것
아직 쪼리와 노바도 실제 화면에서 충분히 많이 확인해야 한다. 무무는 오늘 많이 봤지만, 쪼리는 너무 밈처럼만 가면 피곤할 수 있고, 노바는 너무 시적으로만 가면 실사용 대화가 멀어질 수 있다. 세 캐릭터 모두 "캐릭터답지만 대화가 되는" 선을 찾아야 한다.
레벨업 연출도 더 봐야 한다. 경험치 획득은 작게, 레벨업은 크게라는 방향은 맞지만 실제 화면에서 리듬이 자연스러운지는 계속 봐야 한다. 미션 완료, 공유 카드, 홈, 캐릭터 상세에서 같은 성장 경험이 이어져야 한다.
에셋도 한 번 더 대조해야 한다. 만들어 둔 에셋이 실제로 어디에 쓰였는지, 안 쓰인 에셋이 있다면 일부러 안 쓴 건지 놓친 건지 정리해야 한다. 특히 캐릭터와 보상, 대화 배경, 기억 조각 에셋은 서비스 인상을 크게 좌우한다.
마지막으로, 오늘 작업은 프론트와 백엔드를 따로 고친 게 아니라 하나의 캐릭터 경험을 양쪽에서 맞춘 작업이었다. 다음에 비슷한 문제가 생기면 "API가 되나?"만 보지 말고, "사용자가 이걸 보고 별친구가 살아 있다고 느끼나?"까지 같이 봐야겠다.
'IL > TIL' 카테고리의 다른 글
| Kafka DLT 작업하면서 배운 것들 (0) | 2026.06.11 |
|---|---|
| 별친구 대화에 멀티턴 맥락과 기억 검색 붙이기 (0) | 2026.06.04 |
| 알림 및 장애 대응 Runbook (1) | 2026.06.02 |
| RAG 개인화 리뷰... 튜터님의...? (0) | 2026.06.01 |
| AI 외부 Provider 호출 보호와 Fallback 설계 (0) | 2026.05.29 |