이번 과제는~
https://www.tripbtoz.com
트립비토즈의 기업 과제로 간단한 호텔 예약 서비스를 만드는 것이다!
🥰 열심히 노력한 8팀의 결과물 🥰
(혹시 모를 보안상의 이유로 생략!)
과제를 진행하면서 했던 고민들과 상세한 작업 내용을 기록해보자!
나는 여태껏 제대로 된 반응형 디자인을 구현해 본 경험이 없었다. 기껏해야 flex 혹은 grid를 사용하여 전체적인 너비를 조정하는 것 정도? 하지만, 이번 과제의 서치 바는 꽤 많은 것들이 변해야 했다.
가장 큰 변화는, 검색 조건 변화 이후의 동작이다. 데스크탑 사이즈에서는 검색 버튼을 눌렀을 때 설정된 검색 조건을 반영하여 새로운 데이터를 불러오지만, 모바일 사이즈에서는 검색 조건이 변하는 즉시 반영하여 새로운 데이터를 불러와야 했다. 이 외에도 팝업 변경과 검색 input에 debounce 적용 등, 디자인뿐만 아니라 다르게 처리해야 할 JS 로직이 많았다.
반응형으로, 한 컴포넌트 내에서 모두 처리하면 너무 복잡해질 것 같은데?
컴포넌트를 분리하지 않을 경우, 사이즈에 따른 로직 처리로 코드가 필요 이상으로 복잡해질 것 이고 이에 따라 구현과 버그 수정 모두 힘들어 질 것이라고 판단했다. 따라서 데스크탑 사이즈와 모바일 사이즈를 각각 다루는 컴포넌트를 따로 구현하고 태블릿 사이즈는 모바일 컴포넌트에서 반응형으로 대응하는 것으로 결정했다.
search 폴더 내에 desktop과 mobile 폴더를 만들어 각 사이즈를 대응할 컴포넌트들을 만들었고 search/index.tsx
에서 불러와 현재 사이즈에 맞는 컴포넌트를 렌더링 해주었다. 최종적으로 서치 바를 사용하는 main page에서 search/index.tsx
를 불러와 사용하였다.
반복되는 코드를 줄이자!
기본적으로 같은 기능을 하기 때문에 반복되는 코드가 발생할 수밖에 없다. 따라서 반복되는 로직은 모두 커스텀 훅으로 관리하였고 팝업과 팝업 내부에서 사용하는 컴포넌트는 모두 외부에서 불러오게끔 구현하였다.
검색 조건을 사용하여 데이터를 불러오는 곳은 main/index.tsx
이다. 서치 바는 main/search/
에 존재하는데, 사이즈에 따라 컴포넌트가 별도로 존재하므로 한 깊이 (/desktop or mobile
) 더 들어가야 한다. 또한 서치 바는 3개의 하위 컴포넌트로 구성되어 있으며, 데이터를 설정하는 곳은 하위 컴포넌트에서 띄운 팝업 내부의 컴포넌트이다.
정리를 하자면, 사용자가 설정한 데이터를 총 4단계 위로 퍼 올려야 하는 상황이 발생한 것이다.
props drilling의 담당 일진은...
검색 조건을 전역 상태로 관리하면 간단히 해결할 수 있는 문제지만, 당시 우리 팀은 전역 상태를 도입할 필요가 없는 상황이었다. 때문에, 이 문제 하나만, 이 문제 하나를 해결하기 위해 전역 상태를 도입하는게 맞을까? 라는 생각에 쉽게 결정을 내리지 못했다.
결국(😂) 팀원들에게 현재 상황을 공유하고 의견을 물었는데... 자신있게(?) 하라는 조언을 얻고 recoil
을 사용하여 해당 문제를 해결하게 되었다.
유저가 설정할 수 있는 3개의 검색 조건을 커스텀 훅으로 구현하여 desktop/mobile 컴포넌트에서 재사용 하였으며, 실제 데이터 검색에 사용하는 검색 조건 두 개의 변경을 감지하는 커스텀 훅을 추가로 구현하였다.
데스크탑 사이즈에서는 검색 버튼을 눌렀을 때 검색 조건을 반영하여 새로운 데이터를 불러오고 모바일 사이즈 일 때에는 변경된 검색 조건을 즉시 반영하여 새로운 데이터를 불러온다.
검색 조건을 사용하여 데이터를 불러오는 main page에서 검색 조건(payload)를 state로 관리하고 있으며, 해당 state를 react query
의 key로 사용하고 있다. 즉, main page로부터 내려받은 setPayload를 호출하여 payload를 변경하기만 하면, 별도의 추가 로직 없이 새로운 데이터를 불러올 수 있다.
모바일 사이즈일 경우 변경된 검색 조건을 즉시 반영하여 새로운 데이터를 불러온다. 따라서 text input에 별도의 처리를 하지 않을 경우, '문'을 검색하려고 할 때 'ㅁ', '무', '문' 이와 같이 총 3번의 request가 발생하게 된다. 이러한 불필요한 request를 막기 위해 debounce를 사용하려고 했는데...
값이... 안 변해요... 😇
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
debouncedSet(e.target.value);
};
return { keyword, onChange };
const { keyword, onChange } = useKeywordState();
return <Input value={keyword} onChange={onChange} />;
처음에는 단순하게 접근하여, onChange 내부에서 debounce를 사용하였더니 입력한 대로 값이 변하지 않는 문제가 발생하였다. (onChange에서 set한 value로 input을 제어했기 때문 😢)
제어 방법을 바꿔볼까?
onChange가 아닌 onKeyUp에서 debounce를 사용하고 input의 상위 컴포넌트에서 useEffect로 input value의 변화를 감지하여 input element에 직접 값을 써주었다.
onKeyUp을 사용한 이유는, inputRef.current.value = x;
와 같은 방식으로 input element에 직접 값을 쓸 때 onChange 이벤트가 호출 될 것이라고 생각했기 때문이다. 블로그를 작성하는 지금, 혹시나? 해서 테스트 해봤는데, 직접 값을 쓸 때에는 onChange 이벤트가 호출되지 않는 것을 확인하였다... 😭
이벤트가 어찌 됐든, 이론상으론 문제가 없는 제어 방법인 것 같은데...
input에 값을 빠르게 썼다, 지웠다를 반복하면 지우는 과정에서 이전에 입력됐던 값이 다시 input에 써지는 원인 모를 문제가 간헐적으로 발생하였다. (onChange로 해도 똑같음 😂) 정확한 원인은 파악하지 못했지만, 아마도 useEffect
에서 실시간으로 값을 제어하는 것이 아니기 때문에 키 입력과 업데이트 사이의 알 수 없는 간극(?)이 생긴 것이라고 추측된다.
이러한 문제를 꼭 해결하고 싶었지만... 과제의 제출기한은 정해져있고, input을 꼭 제어해야 하는 상황이 아니었기 때문에, input을 제어하지 않는 방법으로 debounce를 사용했다.
강해져서 돌아와... 꼭 복수하겠습니다...
우리 팀은 데이터를 불러올 때 더 좋은 사용자 경험을 위해 스켈레톤 UI를 적용하였다. 하지만, 예약 내역 페이지로 이동했다가 다시 돌아올 때는 스켈레톤 UI가 나타나지 않고, 꽤 긴 로딩 시간 후에 데이터가 그려지는 문제를 확인하였다.
새로고침 했을 때와 페이지를 이동했을 때 모두 react query
를 통해 request를 보내는 것을 확인하였다. 그래서 처음에는 요청을 보내고 응답을 받는 과정에서 문제가 발생한 것이라고 추측했다.
나름대로 이런저런 확인을 해보던 도중 json-server
의 로그를 확인할 수 있었고, 페이지를 이동한 뒤 돌아올 때에는 응답(상태) 코드로 304를 받는다는 것을 알게 되었다.
여태껏 서버의 응답에서 데이터만 꺼내 썼지, 응답 코드는 확인해본 경험이 없었기 때문에, 처음에는 304가 에러 코드 중 하나인 줄 🤭 알았다.
그래서 304가 뭔데?
이전에 클라이언트가 받아 갔던 리소스가 수정되지 않아서, 새로운 데이터를 보내줄 필요 없이 클라이언트가 본인의 로컬 캐시를 재사용하면 될 때 서버는 304 응답 코드를 보내준다. 이때 응답 메시지의 body
에는 아무런 내용이 없기 때문에 불필요한 네트워크 부하를 줄일 수 있다.
즉, 예약 내역 페이지에서 메인 페이지로 되돌아 올 때는, 이전에 불러왔던 데이터와 동일한 데이터를 요청한 것이기 때문에, 서버로부터 데이터를 새로 받는 것이 아니라 로컬 캐시로 리다이렉트하여 캐시된 데이터를 받아와 그렸던 것이다.
return (
<>
{hotels?.map((hotel) => (
<Card key={hotel.id} {...hotel} />
))}
{isLoading && <Skeleton />}
</>
);
이 문제를 확인했을 당시에는 무한 스크롤을 구현하기 전, 위의 코드와 같은 상태였다. 페이지를 되돌아올 때는 캐시를 사용하기 때문에, isLoading
이 false
라서 스켈레톤 UI 없이 바로 호텔 목록이 표시되었던 것이고 1,000개 이상의 호텔 목록을 한 번에 그려야 했기 때문에 긴 로딩 시간이 발생했던 것이었다.
캐시 사용을 중지해 보면 어떻게 될까?
처음 예상한 시나리오는, 구글 개발자 도구에서 캐시 사용을 중지하면 페이지를 되돌아올 때도 사용할 캐시가 없기 때문에 새로운 데이터를 서버로부터 받아오는 것이었다.
캐시 사용 중지 이후 같은 동작으로 확인해보니, json-server
의 로그에서 304 응답 코드가 사라지고 개발자 도구 네트워크 탭에서 확인할 수 있는 전송 크기 또한 10kb 이상으로 늘어난 것을 확인할 수 있었다. 하지만, 여전히 스켈레톤 UI는 나타나지 않았다.
스켈레톤 UI가 나타나지 않았다는 것은 react query
의 isLoading
이 true
가 되지 않았다는 것이다. 즉, 새로운 데이터를 서버로부터 받아오지 않은 것이다. 또한 스켈레톤 UI 없이 1,000개 이상의 호텔 목록을 한 번에 그리기 때문에, 페이지를 되돌아 올 때 긴 로딩 시간이 여전히 발생하였다.
react query의 cache 넌 누구세요...? 😇
내가 작업을 맡은 기능도 아니였고 이후에 무한 스크롤을 적용할 경우, 한 페이지에 불러올 데이터가 제한되기 때문에 자연스레 사라질 문제였다. 하지만, 도대체 왜 이러나 싶어 이런저런 시도를 해보던 중 아래와 같은 코드를 추가하여 react query
의 cache
기능을 제한하는 설정을 추가해 보았는데...
const queryClient = new QueryClient({
defaultOptions: {
queries: {
cacheTime: 0,
},
},
});
의도한 대로 동작은 하는데...
react query
의 cache
기능을 제한하고 테스트해보니 처음에 의도했던 대로 동작하였다. 내가 고쳐보고 싶었던 것은, 페이지가 이동되기 전에 발생했던 로딩 시간을 페이지 이동에 막힘이 없도록 페이지 이동이 완료된 뒤로 옮기는 것이었다.
정리하면서 곰곰히 생각해 보니, 사실 이 문제는 304 응답 코드보다 react query
의 cache
기능은 어떻게 동작하는가 그리고 isLoading
은 언제 true
가 되는가 이 두 의문이 더 중요한 문제였던 것 같다.
공부를 더 한 다음에 꼭 다시 정리해보자...
과제의 품 자체가 큰 편은 아니었지만, 나 같은 어린이 개발자들은 꼭 한 번씩은 다뤄 바야 할 문제들로 구성되어 있어 굉장히 알찬 시간을 보냈던 것 같다.
지금 개인적으로 급한 일들 얼른~얼른 처리하고 처음부터, 혼자 다시 구현해볼 예정이다. 특히 달력 컴포넌트와 무한 스크롤 쪽은 구현 과정에서 정말 많이 배울 것 같아 굉장히 큰 기대가 된다. 😎