- 발행일
data.data가 data.data.data가 됐다 — 응답 껍데기를 벗기는 자리를 36분 만에 옮겼다
data.data가 data.data.data가 됐다
이 글은 2026년 9월에 당시 커밋 이력을 다시 보며 정리한 것입니다. 날짜는 작업한 날 기준입니다.
직짱건강 백엔드의 응답은 한 겹 껍데기를 두르고 옵니다. 회원 정보를 부르면 몸통이 response.data.data에 있어요. axios가 data를 한 번 감싸고, 서버가 data로 한 번 더 감싼 모양입니다.
2024년 3월 14일 새벽, 마이페이지에 회원 정보가 안 찍혔습니다. 커밋 제목은 "[fix] userInfo data issue"와 "[fix] userInfo data issue 2". 둘 사이가 36분이에요.
첫 커밋 — 껍데기를 화면 쪽으로 넘겼다
0시 36분 6a3ea4a. API 함수가 벗기던 한 겹을 안 벗기기로 했습니다.
export const fetchUserInfo = async () => {
- const response = await axiosInstance.get(fetchUserInfoUrl);
- return {success: true, data: response.data.data};
+ const response = await axiosInstance.get('/api/members/my-page');
+ return {success: true, data: response.data};
그러면 화면이 한 겹 더 벗겨야 합니다. 마이페이지와 프로필 수정 화면의 data.data 일곱 곳이 data.data.data가 됐어요.
- setUserInfo(data.data);
+ setUserInfo(data.data.data);
같은 커밋에서 queryKey를 ['userInfo']에서 ['user_info']로 바꿨고, URL 상수 fetchUserInfoUrl 대신 문자열을 직접 박았습니다. 왜 이렇게 했는지는 커밋에 없습니다. 지금 diff를 읽으면 "어디서 값이 사라지는지 모르겠으니 껍데기를 전부 화면까지 끌고 와서 보자"는 움직임으로 보이는데, 그건 제 추정이에요.
두 번째 커밋 — 되돌리고 훅으로
1시 12분 6f751b6. API 함수는 원래대로 response.data.data와 fetchUserInfoUrl로 돌아갑니다. 대신 화면이 useQuery를 직접 부르던 걸 걷어내고 hooks/react-query에 있던 useQuery_UserInfo를 씁니다.
- const {data, isLoading, error} = useQuery({
- queryKey: ['user_info'],
- queryFn: fetchUserInfo,
- });
+ const {data: userData, isLoading} = useQuery_UserInfo();
useEffect(() => {
- if (data) {
- setUserInfo(data.data.data);
+ if (userData) {
+ setUserInfo(userData?.data);
이 훅은 팀원이 3월 7일에 댓글 화면용으로 만들어 둔 것이었습니다(85fa047). 응답 모양이 타입으로 붙어 있어요.
export const useQuery_UserInfo: () => UseQueryResult<{
success: boolean;
data: {
email: string;
memberId: number;
nickname: string;
profileImage: string;
userId: string;
};
첫 커밋이 껍데기를 화면으로 넘긴 거라면, 두 번째는 껍데기를 벗기는 자리를 API 함수 한 곳에 두고 그 결과 모양을 타입으로 고정한 겁니다. 36분 사이에 "어디서 벗길 것인가"의 답이 바뀌었어요. 같은 응답을 읽는 화면이 둘(마이페이지·프로필 수정)이었으니 한 곳에서 벗기는 쪽이 맞습니다. 그날은 그 이유를 적지 않았습니다.
나흘 뒤 — 에러 문구를 서버에 맡겼다
3월 18일 새벽 6ae8916 "api 형식 통일". 찜 API 두 함수가 상태 코드마다 문구를 갖고 있었습니다.
- let message = "찜한 병원 정보를 가져오는 중 에러가 발생했습니다.";
- switch (error.response.status) {
- case 400:
- message = "잘못된 요청입니다.";
- break;
- case 401:
- message = "인증에 실패했습니다.";
- break;
- case 404:
- message = "찾을 수 없는 정보입니다.";
- break;
- }
- alert(message);
+ alert(errData.message || '찜한 병원 정보를 가져오는 중 에러가 발생했습니다. 다시 시도 해주세요.');
삭제 쪽은 400·401·403·404 넷이었어요. 전부 지우고 서버가 주는 errData.message를 먼저 쓰고, 없을 때만 기본 문구를 씁니다. 마이페이지 API 함수들도 같은 모양으로 줄였고 errCode를 읽던 곳은 errorCode로 바꿨습니다. 서버 응답의 키 이름이 그쪽이었던 거예요.
프런트가 상태 코드를 보고 문구를 고르면 서버가 새 코드를 돌려줄 때마다 프런트를 고쳐야 합니다. 문구는 서버가 정하고 프런트는 보여 주기만 하기로 한 건데, 백엔드와 말로 정했는지는 커밋에 남아 있지 않고 코드만 그렇게 바뀌어 있습니다.
검증
- 그날 확인한 것: 마이페이지에 닉네임·이메일이 찍히는 것. 브라우저에서 눈으로 봤고 기록은 없습니다.
- 이 글을 쓰며 확인한 것:
git show 6a3ea4a의data.data→data.data.data일곱 곳과queryKey변경,git show 6f751b6에서fetchUserInfo가response.data.data로 돌아온 것.git log -S'useQuery_UserInfo'로 훅의 첫 등장이 3월 7일 팀원 커밋인 것. 6ae8916에서 지워진switch두 개의 상태 코드 목록은 diff에서 그대로 옮겼습니다.- 서버 응답이 실제로 어떤 모양이었는지는 프런트 저장소만으로는 알 수 없고 훅의 타입 선언을 그 대신 인용했습니다.
남은 것 · 한계
user_info로 바꾼queryKey가 그대로 남았습니다. 두 번째 커밋이 훅으로 갈아타면서 키는 훅 안의 것을 쓰게 됐는데, 첫 커밋이 왜 키 이름을 바꿨는지는 끝내 모릅니다.- 껍데기를 벗기는 자리를 한 곳으로 모은 건 마이페이지뿐입니다. 찜·설문 API는 여전히 함수마다
response.data와response.data.data가 섞여 있었어요. - 백엔드 한 명과 응답 모양·에러 문구를 코드로 맞춰 가던 방식은 여섯 달 뒤 TripTune에서 검색은 GET, 목록은 POST2024-09-01검색은 GET, 목록은 POST — 하루에 세 번 바꾼 메서드여행지 목록과 검색 API의 메서드를 하루에 세 번 바꿨습니다. 오후 6시에 둘 다 GET, 7시에 page를 쿼리로, 8시 반에 목록만 POST, 밤 11시 반에 검색은 GET으로 확정. 그런데 POST로 바꾸면서 좌표를 params라는 키로 한 번 감싸서 보냈고, 그걸 다음 날 점심에야 풀었어요. 엿새 뒤 검색도 POST가 됐습니다.로 반복됩니다. 그때도 계약을 문서가 아니라 커밋으로 맞췄습니다.
관련 글: 직짱건강 — 백엔드를 기다리지 않고 설문 인터페이스를 먼저 완성한 방법2024-03-31직짱건강 — 백엔드를 기다리지 않고 설문 인터페이스를 먼저 완성한 방법직장인 대상 건강 설문과 위치 기반 병원 추천을 묶은 헬스케어 서비스입니다. 6인 협업 프로젝트에서 프런트엔드로 참여했고, 백엔드 API가 없는 상태에서 JSON 목업으로 설문 플로우를 먼저 완성해 병렬 작업이 가능하게 만든 게 핵심 기여였어요. · 검색은 GET, 목록은 POST2024-09-01검색은 GET, 목록은 POST — 하루에 세 번 바꾼 메서드여행지 목록과 검색 API의 메서드를 하루에 세 번 바꿨습니다. 오후 6시에 둘 다 GET, 7시에 page를 쿼리로, 8시 반에 목록만 POST, 밤 11시 반에 검색은 GET으로 확정. 그런데 POST로 바꾸면서 좌표를 params라는 키로 한 번 감싸서 보냈고, 그걸 다음 날 점심에야 풀었어요. 엿새 뒤 검색도 POST가 됐습니다.