맛집 사이트이기 때문에 좋아요 기능은 필수라고 생각했다. 모든 음식점이 아닌 사용자 개인의 취향에 맞는 음식점만 따로 확인하는 것은 꼭 필요하기 때문에. 그래서 Firestore를 이용하여 간단하게 좋아요 기능을 구현해보았다.
좋아요는 대부분의 페이지에서 확인이 가능하도록 구현하였다. 전체 리스트에서도 쉽게 좋아요 버튼을 누를 수 있게 해두었고, 상세페이지에서도 상호명 옆에 좋아요 버튼을 두었다. 마이페이지에서도 좋아요한 목록 중 일부를 확인할 수 있도록 해주었고, 마지막으로 좋아요 페이지를 만들어 좋아요한 목록을 전부 확인할 수 있도록 해주었다. 총 4개의 페이지에서 좋아요 기능을 이용할 수 있는 것이다. 하지만, 좋아요 버튼 자체가 동작하는 방식은 모든 페이지에서 동일하게 작동하므로, Like component로 만들어서 재사용할 수 있도록 구현하였다.
좋아요를 구현한 방식은 아래와 같다.
- Firestore에 Like Collection을 생성한다. -> 회원가입 시 자동으로 생성
- 좋아요 버튼을 클릭할 경우, Like Collection에서 uid가 id와 일치하는 documnet를 찾아서 likedRestaurants 배열에 좋아요 버튼을 클릭한 place의 place_id를 추가/제거해준다.
- 좋아요 버튼을 클릭할 때마다 모든 list를 불러오기엔 risk가 크므로, 좋아요 버튼은 toggle로 변경해주고, likedRestaurants만 추가/제거해준다.
- 전체 리스트 중 좋아요한 상호를 표시할 경우, Like Collection에서 likedRestaurants를 가져온다. Restaurants Collection에서 place_id가 likedRestaurants에 포함될 경우 좋아요 버튼을 빨간색으로 표시해준다.
리스트를 출력하기 위한 방법은 이전 게시글에서 설명했기 때문에 리스트를 출력하는 방법은 제외하고 좋아요가 동작하는 방식에 대해서만 정리를 하겠다.
좋아요 버튼은 리스트의 우측 상단에 위치하고 있다.
마이페이지 좋아요 목록에서는 좋아요 버튼을 표시하지 않고 있다. 좋아요 버튼이 너무 많다고 느꼈기도 했고, 좋아요 목록이기 때문에 해당 목록이 좋아요된 상호라는 것은 다 알 수 있기 때문이다. (좋아요 페이지가 따로 있으니 굳이 이 목록에서 좋아요 추가/제거를 안 해도 된다고 생각했다.)
import styled, { css } from 'styled-components';
const LikeBox = styled.div`
** 디자인 생략 **
z-index: 10;
${(props) =>
props.type === 'list' &&
css`
position: absolute;
top: 0.4rem;
right: 0.5rem;
@media all and (min-width: 768px) and (max-width: 1023px) {
top: 0.3rem;
right: 0.4rem;
}
@media all and (min-width: 480px) and (max-width: 767px) {
top: 0.2rem;
right: 0.3rem;
}
`}
`;
LikeBox는 하트 아래 하얀색 동그라미를 의미한다. 해당 요소는 상호 이미지 위에 위치해야 하므로, z-index를 10으로 설정해주었다. 마이페이지 좋아요 목록일 경우에는 해당 요소가 표시되지 않기 위해 styled components에 css 변수를 사용하여 조건부 렌더링을 해주었다.
props.type이 list일 경우, position을 absolute로 하고, top과 right를 통해 위치를 지정해주었다. type은 기본값을 list로 설정해주었고, 마이페이지의 경우에만 type 값을 따로 설정해주었다.
<LikeBox type={type} onClick={handleLike}>
<Heart color={isLiked ? '#d80032' : '#ccc'} />
</LikeBox>
Heart는 React-Icons를 사용했고, isLiked에 따라 색상을 다르게 표시하도록 해주었다.
const handleLike = async () => {
try {
const querySnapshot = await getDocs(query(collection(appFireStore, 'likes'), where('uid', '==', id)));
if (!querySnapshot.empty) {
const docSnapshot = querySnapshot.docs[0];
const docRef = doc(appFireStore, 'likes', docSnapshot.id);
const userData = docSnapshot.data();
const updatedList = isLiked
? userData.likedRestaurants.filter((place) => place !== place_id)
: [...userData.likedRestaurants, place_id];
await updateDoc(docRef, { likedRestaurants: updatedList });
setIsLiked(!isLiked);
} else {
Toast.fire({
icon: 'error',
html: '로그인이 필요합니다.',
});
}
} catch (error) {
Toast.fire({
icon: 'error',
html: '오류가 발생했습니다.',
});
}
};
handleLike 함수는 좋아요 버튼을 클릭해주었을 때, 동작하는 함수이다. likes Collection에서 사용자에 해당하는 문서를 가져온다. 이때, 해당하는 문서가 없을 경우, 로그인하지 않은 회원(id가 할당되지 않은 회원)이므로 로그인이 필요하다는 alert를 띄운다. 데이터가 존재할 경우, isLiked가 true이면, likedRestaurants에서 해당 place_id를 제외한 place들로 새로 배열을 만든다. isLiked가 false이면, likedRestaurants에 place_id를 추가해준다. 새로 만들어진 updatedList를 기존 likedRestaurants로 업데이트해준다. 업데이트가 완료되면 isLiked를 toggle해준다.
좋아요 기능은 정말 문제 하나 없이 잘 구현된 기능 중에 하나이다. UI도 예쁘게 잘 나왔고, 구현하면서 Styled components의 css 변수라는 새로운 기능도 사용해보기도 해서 의미가 있는 개발이었던 것 같다. 다만 한 가지 아쉬운 점은 모든 페이지에서 components를 재사용하다보니 좋아요 기능을 사용하는 모든 페이지에서 똑같이 동작하고 있다는 것이다. 무슨 말인고..하면, 좋아요 버튼을 누를 때마다 Firestore에 접근하는 것이 실시간으로 데이터가 변하는 것을 표현할 수 있기 때문에 좋다. 하지만 너무 자주 접근하게 돼서 risk가 너무 컸다. 그러다보니 좋아요 목록에서는 좋아요를 취소해도 실시간으로 리스트가 재렌더링되지 않는다는 게 아쉬웠다. 실시간으로 변경되는 걸 알 수 있다고 들은 것 같긴한데... 이 부분은 더 찾아보고 코드 자체가 많이 바뀌지 않는다면 그 방법을 고려해보는 것도 좋을 것 같다. 현재 개발이 오류 수정 단계이다보니 코드에 큰 변화를 주기에는 부담이 조금 된다. 예상 개발일정보다 일주일을 더... 쓰고 있기 때문에....
튼, 좋아요 개발도 오류없이 잘 작동되고 Firestore에 조금 더 적응한 것 같다.