이번 프로젝트에서 로그인한 사용자가 댓글을 직접 작성하고 수정 및 삭제하는 기능 구현을 맡았다.
댓글을 작성한 산의 이름도 파이어베이스에 저장하여
새로고침 또는 다른 유저들도 산에 대한 리뷰를 확인할 수
있도록 설계했다.
사용자의 정보는 redux 상태관리를 통해 유저 아이디가
존재하면 댓글을 작성할 수 있도록 설계했다.
AddComment.jsx
export default function AddComment() {
const [text, setText] = useState('');
const { user } = useSelector(state => state.user_auth);
const { mountainName } = useParams();
const dispatch = useDispatch();
const handleSubmit = (e) => {
e.preventDefault();
setText('');
if (!user.uid) {
alert('로그인 후 댓글을 작성해주세요!');
return;
}
const newComment = { ...user, id: uuidv4(), comment: text, mountainName, createdAt: new Date() };
addCommentStore(newComment);
dispatch(addComment(newComment));
};
const handleChange = (e) => {
setText(e.target.value);
};
return (
<>
<ScAddCommentLayout onSubmit={handleSubmit}>
<h1>'{mountainName}'에 대한 등산 후기를 들려주세요!</h1>
<div>
<ScComment type="text" value={text} onChange={handleChange} placeholder="댓글을 작성해주세요" />
<ScSubmitBtn>등록하기</ScSubmitBtn>
</div>
</ScAddCommentLayout>
</>
);
}
기존 데이터를 추가할 때 불변성을 유지하기 위해
새로운 배열로 반환해야하지만, 리덕스 툴킷에 immer 기능이 내장되어 있어 push 메서드를 통해 데이터를 추가할 때 수월하다.
commentSlice.js
import {createSlice} from '@reduxjs/toolkit';
import {getComments} from 'shared/firebase';
let initialState = {
comments: [],
};
//댓글 초기값은 파이어스토어에서 데이터 가져오기
initialState.comments = await getComments();
const commentSlice = createSlice({
name: 'comments',
initialState,
reducers: {
addComment: (state, action) => {
const {uid, id, comment, mountainName, displayName, photoURL, createdAt} = action.payload;
state.comments.push({uid, id, comment, mountainName, displayName, photoURL, createdAt});
},
deleteComment: (state, action) => {
state.comments = state.comments.filter(comment => comment.id !== action.payload);
},
updateComment: (state, action) => {
const {id, comment} = action.payload;
state.comments = state.comments.map(cmt => (cmt.id === id ? {...cmt, comment} : cmt));
},
});
댓글을 수정 또는 삭제할 때 댓글을 구분할 수 있는
id가 필요하기 때문에 uuid 라이브러리를 설치한다.
또한 redux에서 관리하고 있는 유저의 uid와 동일한
유저 즉, 댓글을 작성한 본인만 수정 또는 삭제가 가능하도록 구현한다.
파이어베이스 공식 문서에서 파이어스토어에 저장된 데이터를 삭제하는 코드가 자세하게 나와있지 않아 많은 검색이
필요했다. 이 코드는 쿼리를 사용하지 않고 단순하게
문서 id를 통해 삭제하는 코드만 있었다.
내가 생각한 댓글 삭제 방식은 삭제 버튼을 클릭한
댓글 id를 전달해주어 댓글 데이터들 중에서 동일한 댓글 id만 삭제를 하고 싶었다. 조건문을 추가할 수 있는 파이어베이스 쿼리문에 대해 찾아보았다.
firebase.js
- 삭제를 원하는 댓글 id와 동일한 데이터만 쿼리를 통해 데이터를 뽑아온다. 모든 댓글의 id는 유일한 값이므로 조건에 만족하는 데이터는 항상 한개여서 data.docs[0]을 통해 접근할 수 있다.
- .ref는 deleteDoc함수는 reference를 필요하기 때문에 ref 속성을 통해 해당 레퍼런스를 넘겨준다.
export const deleteCommentStore = async (id) => {
const deleted = query(collection(db, 'comments'), where('id', '==', id));
const data = await getDocs(deleted);
return await deleteDoc(data.docs[0].ref);
};
💡 해결방안)
수정 상태 값을 수정하고자 하는 댓글의 id값을 부여해 동일한 id안 경우에만 수정모드로 변경해준다.
수정 중인 상태 초기값을 null로 설정.
수정 버튼 누르면 수정할 댓글의 id를 부여.
해당하는 id만 수정할 수 있는 textarea나오도록 설정.
수정이 다 끝나거나 취소 버튼을 클릭하면 기존 초기값으로 설정.
수정된 사항이 없거나 공백으로 변경한 경우 수정 완료 버튼 클릭할 수 없도록 설정.
수정 완료 버튼을 클릭하면 파이어베이스 updateDoc을 통해 값을 변경하고, 또한 commentSlice 모듈 역시 값을 업데이트 해준다.
CommentList.jsx
export default function CommentList() {
const [editComment, setEditComment] = useState();
const [text, setText] = useState('');
const { comments } = useSelector(state => state.comments);
const { user } = useSelector(state => state.user_auth);
const [getComments, setGetComments] = useState(comments);
const dispatch = useDispatch();
const filterComments = getComments.filter(comment => comment.mountainName === mountainName);
const handleEdit = (id) => {
//수정하고 있는 댓글 id값 부여여
setEditComment(id);
};
const handleUpdate = (id) => {
if (window.confirm('수정하시겠습니까?')) {
updateCommentStore(id, text);
dispatch(updateComment({ id, comment: text }));
//수정 모드 해제
setEditComment(null);
}
};
useEffect(() => {
//댓글이 추가 또는 삭제될 때 마다 업데이트
setGetComments(comments);
}, [comments]);
return (
<ScCommentListLayout>
{filterComments?.map(c => {
const { id, uid, displayName, comment, photoURL, createdAt } = c;
return (
<li key={id}>
<ScCommentBox>
{editComment === id ? (
<ScEditComment defaultValue={comment} onChange={e => setText(e.target.value)} />
) : (
<ScComment>{comment}</ScComment>
)}
<ScUserInfo>
<img src={photoURL} alt="avatar" />
<p>{displayName}</p>
</ScUserInfo>
</ScCommentBox>
<ScButtonBox>
{user.uid && uid && user?.uid === uid && editComment !== id && (
<button onClick={() => handleEdit(id)}>
<FaPencilAlt />
</button>
)}
{editComment === id && (
<button disabled={text === comment || !text} onClick={() => handleUpdate(id)}>
<FaCheck />
</button>
)}
{editComment === id && (
<button onClick={() => setEditComment(null)}>
<FaUndoAlt />
</button>
)}
{user.uid && uid && user?.uid === uid && (
<button onClick={() => handleDelete(id)}>
<FaTrashAlt />
</button>
)}
</ScButtonBox>
<time>{new Date(createdAt).toLocaleString()}</time>
</li>
);
})}
</ScCommentListLayout>
);
}
firebase.js
댓글 삭제도 마찬가지로 기존에 작성한 쿼리와
동일하게 동일한 댓글 id만 찾아 deleteDoc 함수를 사용한다.
export const deleteCommentStore = async (id) => {
const deleted = query(collection(db, 'comments'), where('id', '==', id));
const data = await getDocs(deleted);
return await deleteDoc(data.docs[0].ref);
};