왜인지 모르겠지만, 코드를 짜다보면 어느순간 아무것도 진행이 되지 않은 시기가 온다.
이번 프로젝트에서 가장 당황하였던 것은 그 시기가 프로젝트 시작부터 찾아와버렸다는 점이다. 지친거였을까..
시간은 흘러가는데 무엇인가 하고 싶진 않은 마음을 진정시키려 하는데, 에어비엔비 검색창을 보니 조급함만이 살아나버렸다..
적어도 지금까지 만들어온 nav 와는 사뭇 다른 느낌이었다. 동적이면서도 절제된듯한 UI 에 놀랍기도, 욕심도 나기 시작했다. (근데 왜 의욕이 안생기는거지). 사라진 의욕만 돌아와 준다면 좀 더 재미있게 작업할 수 있을 것 같은데!
왠지 비슷하게라도 구현하지 못할거 같은 느낌때문에 시작을 못하는게 아닐까 생각이 들어서, 동기들도 열심히 하는데 이리저리 생각만 하지말고 일단 시작하자라고 굳건히 마음을 먹고 다시 시작해봤다. 하다보면 의욕은 분명 돌아올꺼야 분명
Component - Search
에어비엔비에서 검색 부분을 보게 되면, 위치와 달력, 그리고 여행자 수를 각각의 모달창에서 선택이 가능하게 되어있다. 모달창 자체를 컴포넌트화 시키는 것이 효율적인지는 판단이 서진 않았지만, 적어도 위치, 달력, 여행자 수 3가지 선택창은 따로 컴포넌트로서 관리하는것이 유지보수에도 유리할 것이라 판단하였고, 실제 Search 컴포넌트를 구성하는 코드도 좀 더 보기 좋아질것이라 생각이 들었다. 게다가 달력의 경우 Date Picker 라이브러리를 사용해야 하기에, 혹여나 변수가 생길 가능성이 있어 독립적인 컴포넌트로 관리하자고 계획을 세웠다.
우선적으로 구조를 잡을 때, 모달창은 결국 position = absolute 로 될 것이며, 그 위 타원형의 검색창 부분이 relative 로서 자리잡아야 화면의 움직임에 대해 모달창의 위치 역시 대응하여 움직일 것이다. 이러한 사실을 바탕으로 부모 자식의 관계를 잘 고려하여 레이아웃을 작성하였다.
<SearchSection>
{locationModalIsOpen || dateModalIsOpen || guestModalIsOpen ? (
<ModalOverLay onClick={overLayClick} ref={modalRef} />
) :
null}
<SearchBarContainer>
<WrapperLocationContainer
className={currentId === 1 ? 'is_open' : null}
onClick={() => {
setLocationModalIsOpen(true);
clickHandler(1);
}}
>
<DatePickerLabel>여행지</DatePickerLabel>
<SearchBarSpan>{location}</SearchBarSpan>
</WrapperLocationContainer>
<WrapperDatePicker>
<WrapperDatePickerInput
className={currentId === 2 && !endDate ? 'is_open' : null}
onClick={() => {
setDateModalIsOpen(true);
clickHandler(2);
}}
>
<DatePickerLabel>체크인</DatePickerLabel>
<SearchBarSpan>
{startDate
? `${startDate.getMonth() + 1}월 ${startDate.getDate()}일`
: '날짜 선택'}
</SearchBarSpan>
</WrapperDatePickerInput>
<WrapperDatePickerInput
className={currentId === 2 && endDate ? 'is_open' : null}
onClick={() => {
setDateModalIsOpen(true);
clickHandler(2);
}}
>
<DatePickerLabel>체크아웃</DatePickerLabel>
<SearchBarSpan>
{endDate
? `${endDate.getMonth() + 1}월 ${endDate.getDate()}일`
: '날짜 선택'}
</SearchBarSpan>
</WrapperDatePickerInput>
</WrapperDatePicker>
<WrapperGuestContainer
className={currentId === 3 ? 'is_open' : null}
onClick={() => {
setGuestModalIsOpen(true);
clickHandler(3);
}}
>
<DatePickerLabel>여행자</DatePickerLabel>
<SearchBarSpan>
{guest !== 0 ? `성인 ${guest}명` : '게스트 추가'}
</SearchBarSpan>
<IconContainer onClick={toSearchUserInfo}>
<i class="bx bx-search" />
</IconContainer>
</WrapperGuestContainer>
{ModalComponent[currentId]}
</SearchBarContainer>
</SearchSection>
);
};
위 코드는 전체 Search 컴포넌트의 구성이다. 스타일은 styled-component 로 구성하였고, 위치나 달력, 여행자 수 모달창이 나타나게 될 때, 전체 overlay screen 이 나타나게 된다. (왜냐하면 바깥 부분을 클릭할 때 모달창이 닫혀야 하기 때문이다.)
따로 다루긴 뭐해서 미리 살펴보자면, overlay screen 의 발동 조건은 boolean 으로 결정되는데, 3가지의 모달창 중 하나라도 true 라면 overlay 화면이 나타나게 된다. (즉, locationModalIsOpen... 등 3가지 중 하나라도 true 인 경우)
useRef 를 이용하여 DOM 에 접근해보자
뭐 overlay screen 이 나오는데 까지는 성공하였다 치더라도, 이제 이 화면을 클릭할 시 모달창 및 검색창이 닫아져야 하지 않을까? (나중에는 profilecontainer 역시 닫혀야 한다)
여러 방법이 있는데, 그 중 useRef 를 활용하는 방법부터 살펴보겠다. (추후에 e.stopPropagation() 을 이용하는것도 다루겠음)
const overLayClick = e => {
if (modalRef.current === e.target) {
setDateModalIsOpen(false);
setLocationModalIsOpen(false);
setGuestModalIsOpen(false);
setCurrentId(0);
setToggleNavbar(true);
}
};
return (
<SearchSection>
{locationModalIsOpen || dateModalIsOpen || guestModalIsOpen ? (
<ModalOverLay onClick={overLayClick} ref={modalRef} />
) : null}
<SearchBarContainer>
// 생략
갑작스럽게 modalRef 가 나왔다. 사실은 Search 컴포넌트보다 더 상위인 Nav 에서 props 로 내려준 것이고, 쉽게 이해하려면 그냥 위쪽에 modalRef = useRef() 가 있다고 생각하자.
useRef 를 통해 DOM 에 접근을 할 수 있는데, 사용방법고 간단하다. 접근하고자 하는 요소에 ref={modalRef} 로 기입을 하면 접근이 가능하다. 마우스로 클릭을 했을 때, 즉, e.target 이 overlay screen 내부라면 모달창을 종료하면 되는데, 이때 이 조건을 modalRef.current === e.target 으로 표현하는것이 가능하다. 이렇게 조건을 표현하는게 가능해지면, 그 이후 모달창을 닫는건 쉽다. 위 처럼 boolean 값들을 모두 반대로 해주고, currentId 를 초기값으로 돌려주면 된다.
(참고로 위 setToggleNavbar 의 경우 작은 Nav 로 변경해주는 boolean 값이다. 나중에 다룰예정)
하나의 ID 값으로 여러개의 상태를 관리하자.
const [currentId, setCurrentId] = useState(0);
const clickHandler = id => {
setCurrentId(id);
};
동적인 Nav 이기에 사용자의 마우스 클릭에 따른 여러 상태변화가 필수적이다. 이를 대처하기 위해 모든 부분을 state 로 관리해야 할 수도 있지만 줄여볼것은 줄여봐야 한다고 생각이 든다.
위 currentId 는 크게 검색창의 작은 블럭 이동시 바탕화면의 색 변화, 실제 모달창의 켜짐 여부 2가지를 관장하게 된다.
바탕화면의 색 변화는 맨 위 에어비엔비 사진에서 알 수 있듯이, 여행지 부분은 하얀색이며 나머지 부분은 짙은 회색임을 알 수 있는데, 나타나는 모달창에 대응하여 저 부분도 변경이 된다.
<WrapperLocationContainer
className={currentId === 1 ? 'is_open' : null}
onClick={() => {
setLocationModalIsOpen(true);
clickHandler(1);
}}
>
// 생략
const WrapperDatePickerInput = styled.div`
display: flex;
flex-direction: column;
gap: 0.5rem;
width: 130px;
height: 65px;
padding: 14px 24px;
&.is_open {
background-color: white;
box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 12px;
}
:hover {
background-color: rgba(0, 0, 0, 0.2);
}
border-radius: 32.5px;
`;
위에서 알 수 있듯이, class is_open 이 추가됨에 따라 배경화면의 색상을 변화시킨다.
onClick 이벤트를 통해서 setCurrentId 를 실행시킴으로서 그 숫자에 따라 조건식으로 class 를 WrapperDatePickerInput 에 부여한다.
그 다음 실제 모달창의 생성 여부를 조절한다.
const ModalComponent = {
1: (
<Location
startDate={startDate}
endDate={endDate}
setLocation={setLocation}
/>
),
2: <Calender startDate={startDate} endDate={endDate} onChange={onChange} />,
3: (
<GuestType
guest={guest}
increseNum={increseNum}
decreseNum={decreseNum}
disabled={disabled}
/>
),
return (
// 생략
{ModalComponent[currentId]} // Id 에 따라서 모달창 컴포넌트가 변화한다.
</SearchBarContainer>
</SearchSection>
ModalComponent 라는 객체에서 key 값 1,2,3 에 따라서 상응하는 컴포넌트가 다르다. 여기서 currentId 와 공유하는 부분이 보이지 않은가? 바로 key 값이다. currentId 에 대응하는 key 값을 통해 value 인 component 를 가져오는 로직을 구상하였다.
이로서 하나의 state 로 2가지의 변화에 대응하였다.
서버에 검색 결과를 보내주기 (나중에 다시 보는것을 추천)
검색을 하였으니 이제 필터로 거른 데이터를 서버에 넘겨주어야 하는데, 사실 이부분은 뒤에 다루게 될 위치 및 달력, 사용자 수 컴포넌트를 다루고 나서야 좀 더 자세히 이해가 될 것이라 생각이 든다. 그래서 우선은 코드를 참고만 하시길...
const leftPad = value => {
if (value >= 10) {
return value;
}
return `0${value}`;
};
const toStringByFormatting = (date, delimiter) => {
const year = date.getFullYear();
const month = leftPad(date.getMonth() + 1);
const day = leftPad(date.getDate());
return [year, month, day].join(delimiter);
};
const toSearchUserInfo = e => {
e.stopPropagation();
const startDay = startDate
? `&check_in=${toStringByFormatting(startDate, '-')}`
: '';
const endDay = endDate
? `&check_out=${toStringByFormatting(endDate, '-')}`
: '';
const selectLocation = location ? `&address=${location}` : '';
const totalGuest = guest ? `&maximum_occupancy=${guest}` : '';
navigate(`/list?${startDay}${endDay}${selectLocation}${totalGuest}`);
fetch(
`${BASE_URL}/rooms?${startDay}${endDay}${selectLocation}${totalGuest}`
)
.then(res => res.json())
.then(data => console.log(data));
setDateModalIsOpen(false);
setLocationModalIsOpen(false);
setGuestModalIsOpen(false);
setCurrentId(0);
setToggleNavbar(true);
};
서버에 필터된 데이터를 보낼 때는 query parameter 로 보내는것이 일반적이다. 아마 다른 사이트들에서 검색을 하다보면 상단 url 이 변화되는것을 본적이 있을 것이다. 보통 url(프론트단에서 페이지 이동시 다루는) 과 api 주소는 차이가 있다. 다만, 이 두가지 주소에서 필터된 부분을 동일시 하게 설정하게 되면, url 의 변경과 동시에 데이터를 서버에 보낼 수가 있다. 위 코드는 그러한 과정을 담고 있다.
백엔드에서는 받을 수 있는 데이터의 형식이 정해져있고(혹은 미리 정해둔 다음 명세서로 보여준다) 프론트는 그에 맞게 데이터를 보내주어야 한다. 하나의 예시로 날짜를 둘 수가 있는데, 당시 백엔드측에서는 날짜 데이터를 yyyy-mm-dd 식으로 받을 수 밖에 없다고 선언(?) 을 해버리는 바람에, 내가 그에 맞게 데이터를 가공하여 보내주어야 했다.
이를 나타내 주는 함수가 toStringByFormatting(date, parameter) 이다. 달력으로부터 받아오는 startDate, endDate 를 백엔드가 원하는 데이터 형식으로 변경해준다.
형식에 맞게끔 데이터를 가공하였다면 이제, 백엔드로 데이터를 전송시키면 되는데 api 주소에다가 필터된 데이터들을 나열해서 전달해주면 된다. 방법은 위처럼 하면 되는데, 예시를 하나 들어보자면,
- 8월 20일~ 8월 25일 까지 경기도 지역에서 성인 3명이 지낼 숙소를 검색
이 경우 백엔드와 연결할 API 주소는
'BASE_URL/rooms?&check_in=2022-08-20&check_out=2022-08-25&adress=경기도&maximum_occupancy=3'
처럼 형성이 되고, 실제 서버에서 api 주소를 받을 때, 이에 맞는 데이터를 프론트에게 전달해준다. 다만 데이터를 받는 페이지나 컴포넌트는 우리가 따로 지정을 해야한다. 만일 데이터를 필요로 하는 페이지가 목록 페이지라면,
- 데이터를 전송하자마자 (즉, 검색을 하자마자) 목록 페이지로 이동을 한다.
- 목록페이지가 렌더링 될 때, 서버로 부터 필터링 된 데이터를 받아온다
- 받아온 데이터를 바탕으로 숙소 목록을 렌더링 해서 사용자에게 보여준다.
즉, 이러한 과정을 위해서는 데이터를 보냄과 동시에 useNavigate 를 활용하여 routing 을 시켜줘야 한다. 이 외에도 useParams 를 이용하여 목록 페이지에서 서버에게 필터링 된 데이터를 받아오는 과정 등등 좀 더 많은 단계가 있는데, 추후 설명할 수 있을거같으니 지금은 여기서 마무리 하겠다.
검색이 마무리 되었으니 당연히 모달창들은 다 닫아줘야 할 테니(안해주면 목록페이지에도 켜져있던것으로 기억한다), 위 코드 하단부처럼 조치를 취해준다. 이렇게 하면 얼추 검색까지의 과정이 마무리 된다.
쓰다보니 더 길어질 거 같아서 일단 끊고, 다음에는 실제로 연관된 위치, 여행자 수 그리고 대망의 달력 컴포넌트를 다뤄보겠다.
진짜 Date Picker 다룰 떄 CSS 때문에 정말 고생 많이했던 기억이.... 추후 사용자에게 조금이라도 도움이 될 수 있음 좋겠다.
'Programing > React' 카테고리의 다른 글
2차 프로젝트 - 에어비엔비 우측 상단 프로필 버튼(4) (0) | 2022.09.03 |
---|---|
2차 프로젝트 - 에어비엔비 상단 검색창 - Location, Calender, User(3) (2) | 2022.08.31 |
2차 프로젝트 - 에어비엔비 상단 검색창(1) (0) | 2022.08.24 |
테라로사 사이트 클론 프로젝트 - 2주차 (Search 구현) 및 회고 (0) | 2022.07.31 |
테라로사 사이트 클론 프로젝트 - 1주차 (Cart Page) (0) | 2022.07.31 |