이번 프로젝트는 마이리얼트립 웹사이트를 클론하는 방식으로 진행되었다.
기존의 웹사이트에는 항공권 · 투어/티켓 · 국내숙소 · 해외숙소 등 다양한 카테고리가 있었지만, 우리는 국내숙소 페이지를 메인으로 잡고 진행하게 되었다.
나는 그 중에서도 지역, 날짜, 인원수를 선택하면 넘어가게 되는 검색리스트
페이지를 구현하게 되었다.
const [isLocation, setIsLocation] = useState(false);
const [locationInput, setLocationInput] = useState("");
const openLocation = () => {
setIsLocation(true);
};
const closeLocation = () => {
setIsLocation(false);
};
<s.LocationInput
onChange={e => {
e.preventDefault();
setLocationInput(e.target.value);
}}
onFocus={openLocation}
onBlur={closeLocation}
/>
{isLocation && (
<LocationModal
locationInput={locationInput}
setLocationInput={setLocationInput}
/>
)}
const sortedList = searchList.filter(hotels => {
return (
hotels.address.includes(locationInput) ||
hotels.name.includes(locationInput)
);
});
overflow-y: hidden;
을 통해 모달창 바깥으로 나열되는 데이터는 숨겨주었다. {sortedList.map(list => {
return (
<LocationListBox key={list.id}>
<HotelIcon icon={faHotel} size="2x" />
<div
onClick={() => {
setLocationInput(list.name);
}}
>
<HotelName>{list.name}</HotelName>
<HotelLocation>{list.address}</HotelLocation>
</div>
</LocationListBox>
);
})}
캘린더는 ant-design 라이브러리를 사용하여 구현했다.
( ant-design 라이브러리 설치 및 사용 방법 👈 참고! )
라이브러리 사용은 이번이 처음이었는데 편리해보이면서도 기능 하나, 스타일 값 하나 바꾸는 게 생각보다 복잡해서 애를 먹었다..^^;
캘린더 라이브러리는 애초에 날짜 input창을 누르면 아래로 캘린더모달이 열리게 끔 구현되어 있어서 style값만 약간 바꾸고 입력한 값을 배열의 형태로 state에 저장해주었다.
const [date, setDate] = useState();
<StyledDatePicker picker="date" onChange={getDateValue} />
const StyledDatePicker = styled(DatePicker.RangePicker)`
position: absolute;
left: 50px;
top: 10px;
`;
이번 프로젝트에서는 scss 대신 styled-component를 사용했는데, 이렇게 하니 캘린더 모달은 style 속성 변경이 가능하지만 input창 style값이 도저히 변경이 안됐다. 아마 DatePicker.RangePicker 태그 바깥에 input창이 있는듯 했다...
그래서 결국은 scss를 사용하게 되었다. 개발자도구를 열어 input창의 classname을 확인 후 하나씩 style값을 지정해주었다.
.searchContainer {
.ant-picker.ant-picker {
border: 0 !important;
padding: 4px;
}
.ant-picker:focus {
outline: none;
}
.ant-rate {
color: #51abf3;
margin-top: 20px;
}
.ant-rate svg {
width: 35px;
height: 35px;
}
.ant-rate path {
width: 35px;
height: 35px;
}
.ant-picker-input input {
font-size: 14px;
font-weight: bold;
}
.ant-picker-suffix {
display: none;
}
}
인풋창 클릭시 모달창이 열리고 닫히는 건 location 모달과 똑같이 구현했고, 모달 내에서 +-버튼을 클릭할 때 어른과 아이의 수가 따로따로 변경되고 상단 Input창에서는 둘의 합계가 보이도록 구현하는 것에 집중했다.
const plusAdult = () => {
setCountAdult(countAdult + 1);
};
const minusAdult = () => {
if (countAdult > 1) {
setCountAdult(countAdult - 1);
}
};
const plusChild = () => {
setCountChild(countChild + 1);
};
const minusChild = () => {
if (countChild > 0) {
setCountChild(countChild - 1);
}
};
<s.SearchBox onClick={openHeadcount} onBlur={closeHeadcount}>
<s.Number>{countAdult + countChild}명</s.Number>
</s.SearchBox>
{isHeadcount && (
<HeadcountModal
setIsHeadcount={setIsHeadcount}
countAdult={countAdult}
setCountAdult={setCountAdult}
countChild={countChild}
setCountChild={setCountChild}
/>
)}
const LIMIT = 40;
const updateOffset = buttonIndex => {
const offset = LIMIT * buttonIndex;
const queryString = `?limit=${LIMIT}&offset=${offset}`;
navigate(queryString);
};
fetch(
`${IP}/products?${
location.search.split("?")[1] || `limit=${LIMIT}&offset=0`
}`
)
useLocation과 useNavigate함수를 이용해서 버튼을 클릭할 때마다 offset값이 변경되도록 구현했다.
또한 클릭된 버튼만 색상이 변경되도록 하기 위해 clicked와 id값이 일치하는지를 확인하고, props와 삼항연산자를 이용해 style속성이 변경되도록 구현했다.
export default function Buttons({ updateOffset, id, clicked, setClicked }) {
const clickButton = id => {
setClicked(id);
};
return (
<Button
onClick={e => {
clickButton(id);
updateOffset(id - 1);
}}
primary={clicked === id}
>
{id}
</Button>
);
}
background-color: ${props => (props.primary ? "#52ABF3" : "white")};
color: ${props => (props.primary ? "white" : "#52ABF3")};
다른 querystring을 연결하는 과정에서 페이지네이션과 같이 작동하게 하는 방법을 못찾았다. 처음에는 limit값을 10으로 설정해서 한 페이지당 숙소가 10개씩 보이도록 했는데, 숙소 카테고리, 가격, 등급 등의 조건을 설정하고 필터링 된 화면에서 다시 페이지버튼을 누르면 필터링 된 값이 모두 사라져버린다...
이런저런 시도를 해보았으나 시간의 부족으로 결국 페이지네이션기능은 포기하고, limit값을 40으로 설정해 모든 데이터가 다 1번페이지에 나열되게끔 만들었다.. ㅎ ㅏ아..ㅠ
이번에는 프론트에서 데이터를 필터링해 보여주는 것이 아니라 백엔드에서 이미 필터링 된 데이터를 query string과 end point를 이용해 보여주는 방식으로 진행했다.
따라서 값이 계속 변화하는 부분들을 변수로 저장하고, useNavigate와 useLocation을 사용하여 url주소를 변경시켜주었다.
필터링 되어야 하는 조건들은 아래와 같다.
카테고리 버튼도 한번에 하나만 클릭되고 색상이 변경되도록 하기 위해 페이지네이션 버튼과 똑같은 로직으로 구현해주었다. 그리고 클릭된 부분을 state에 저장해주었다.
처음에는 html의 <input type = "range" />
를 이용해 구현했으나, ant-design 내에 slider 라이브러리가 있는 걸 알고 그걸 사용했다.
const [price, setPrice] = useState([0, 1000000]);
<s.FilterText>
{price[0]
.toString()
.replace(/\B(?<!\.\d*)(?=(\d{3})+(?!\d))/g, ",")}
원 ~
{price[1]
.toString()
.replace(/\B(?<!\.\d*)(?=(\d{3})+(?!\d))/g, ",")}
{price[1] === 1000000 ? "원 이상" : "원"}
</s.FilterText>
<Slider
range={true}
values={price}
max={1000000}
step={10000}
onAfterChange={price => {
setPrice(price);
}}
/>
마찬가지로 라이브러리를 사용했고, 클릭된 부분이 state에 저장되도록 했다.
const [rates, setRates] = useState(0);
<Rate onChange={getRates} />
const [ischecked, setIsChecked] = useState([]);
<s.FilterTitle>시설</s.FilterTitle>
{FACILITIES.map(el => {
return (
<Facilities
key={el.id}
name={el.facility}
// checked={el.name}
ischecked={ischecked}
setIsChecked={setIsChecked}
/>
);
})}
search
start_date
퇴실 날짜: end_date
guest
category
min_price
최대 가격: max_price
grade
amenities
모든 엔드포인트는 &로 연결한다.
let amenities = ischecked.map(el => {
return `&amenity=${el}`;
});
amenities = amenities.join("");
const filter = () => {
navigate(
`?search=${locationInput}&start_date=${date[0]}&end_date=${
date[1]
}&guest=${countAdult + countChild}${
selectedCategory && selectedCategory !== `전체`
? `&category=${selectedCategory}`
: ``
}&min_price=${price[0]}&max_price=${price[1]}${
rates ? `&grade=${rates}` : ``
}${ischecked.length !== 0 ? amenities : ``}`
);
};
처음 목표한 부분의 70%정도를 달성한 것 같다.
이번 프로젝트에서 styled-component와 리액트 라이브러리를 처음 사용해보았는데 생각보다 어려워 시간이 많이 걸렸다.
또 백엔드에서 이미 다 필터링 한 데이터를 넘겨주기 때문에 프론트에서는 할 일이 많이 없을 거라 생각했는데, 계속 변화하는 값을 state에 저장하고 다시 query string으로 구현하는 과정에서 많이 헤맸다.
그래도 1차 때랑 비교하면 UI 구성도 하루이틀만에 끝내고 나머지는 온전히 기능구현하는 데에 집중한 것 같다. 또 같은 기능을 구현할 때도 어떤 식으로 코드를 짜야 효율적인가에 대한 고민을 많이 한 것 같다.
Map API를 꼭 써보고 싶었는데 시간의 제약으로 구현해보지 못한 게 너무너무 아쉽다..ㅠㅠ API를 불러오는 것까지는 성공했지만 해당되는 데이터들의 위치를 전부 마커로 찍는 부분을 구현하지 못했다.. 백엔드에서 위도경도 데이터들도 모두 넘겨주셨는데.. ㅎ ㅏ아..🥲 이부분은 개인적으로 공부해서 꼭 만들어보고 싶다!