본문 바로가기
프로젝트/회고

팀 프로젝트 - 커뮤니티 프로젝트

by dev__log 2024. 2. 15.

📢 프로젝트 소개

  • 프로젝트 명 : [deve11og] 질문 및 자유로운 주제로 소통하는 커뮤니티 사이트입니다.
  • 프로젝트 일정 : 2/7 ~ 2/15
  • 깃 허브 링크 : 바로가기
  • 배포 링크 : 바로가기
  • 개발 환경 : react, npm, firebase(Storage, Authentication, Cloud Firestore)
  • 배포 환경 : vercel
  • 협업툴 : git, notion, slack
  • 주요 의존성 라이브러리 : Redux, React Router, styled-components

 

💻 프로젝트 구현 기능

  • 로그인
  • 회원가입
  • 게시물 작성 및 수정
  • 댓글 작성(수정, 삭제)
  • 마이페이지

 

😀 담당 구현 기능

  • 마이페이지
    • 닉네임 변경
    • 프로필 이미지 변경 및 삭제
    • 내 게시물 보기
  • 댓글
    • 댓글 작성 및 수정
    • 댓글 삭제
    • 작성자만 수정 및 삭제 버튼 보이도록 조건부 렌더링
    • 유효성 검사

 

프로젝트 과정

1. 기획

처음에는 노래 관련 주제로 시작하였으나... 프로젝트에 '노래'라는 요소를 녹여내기가 어려웠다. 

그래서 프로젝트 둘째 날, 무려 설 연휴가 시작되기 전날 저녁부터 그다음 날 새벽까지 기획을 다시 하고 잤다. 

설 연휴에는 모이기 힘들 것 같아서 기획을 꼭 정해야만 했다.

 

팀원들과 새벽까지 기획한 끝에 새로운 와이어프레임을 만들어냈다. (정말 모두 고생하셨습니다ㅠㅠ)

 

2. 설계

팀원들과 와이어프레임을 토대로 firebase에 사용할 데이터 목록을 정리했다.

 

user

필드명 유형 설명
userId string(pk) 회원 pk
email string 회원 로그인 ID
pwd string 비밀번호
nickname string 닉네임
userImg string  회원 프로필 이미지

 

board

필드명 유형 설명
boardId string(pk) 게시물 pk
title string 제목
contents string 내용
regDate date 등록일
liked number 좋아요수
category string 카테고리
thumbnailId string 썸네일 이미지 ID
imgUrl string 썸네일 이미지 url
nickname string 닉네임
userId string(fk) 회원 fk

 

comments

필드명 유형 텍스트
id string(pk) 댓글 pk
boardId string(fk) 게시물 fk
userId string (fk) 회원 fk
contents string 내용
regDate string 등록일

 

3. 개발

[마이페이지]

처음에는 유저 프로필 이미지 업로드를 할 때 무조건 이미지를 선택하고 등록 버튼을 눌러서 등록을 해야 했다. 

등록 버튼을 누르지 않고 해보려고 했지만 잘 되지 않아서 일단 맡은 다른 기능들을 완성하고 나서 다시 이미지 업로드 부분으로 돌아왔다. 

 

기존에는 등록 버튼을 눌러야지만 등록이 가능했다. 

input file onChange에서 실행되는 fileSelect 부분에서 이미지 등록까지 하고 싶었지만, 잘 안 됐다... 

마음이 촉박해서 더 구현을 못했던 것 같다...

const MyPage = () => {
  const fileSelect = (event) => {
    setSelectedFile(event.target.files[0]);
  };

  const handleUpload = async () => {
    //파일 업로드
    if (window.confirm('선택한 이미지로 업로드를 진행할까요?')) {
      const imageRef = ref(storage, `${user_id}/${selectedFile.name}`);
      await uploadBytes(imageRef, selectedFile);

      //파일 업로드 후 state로 저장
      const downloadURL = await getDownloadURL(imageRef);
      setUserInfo((prev) => {
        return {
          ...prev,
          user_img: downloadURL
        };
      });
      const newData = {
        nickname,
        user_id,
        user_img: downloadURL,
        email
      };
      dispatch(updateImage(downloadURL));

      //파일 업로드 db 업데이트
      const docRef = doc(db, 'user', TEST_ID);
      await setDoc(docRef, {
        ...userInfo,
        user_img: downloadURL
      });
      setSelectedFile(null);
      alert('업로드가 완료되었습니다.');
      const auth = getAuth();
      updateProfile(auth.currentUser, {
        photoURL: downloadURL
      })
        .then(() => {
          setSelectedFile(null);
          alert('업로드가 완료되었습니다.');
        })
        .catch((error) => {
          console.log(error);
          alert('에러가 발생했습니다. 다시 시도해주세요.');
        });
    } else {
      alert('업로드를 취소했습니다.');
    }
  return (
    <LeftAreaStyle>
      <FigureStyle>
        <img src={!user_img ? DEFAULT_IMAGE : user_img} onError={errorImage} alt="유저 이미지" />
      </FigureStyle>
      <FileLabelStyle>
        이미지 업로드
        <input type="file" onChange={fileSelect} accept="image/*" />
      </FileLabelStyle>
      {!isImageRemovable ? <></> : <BtnBlackText onClick={handleRemove}>이미지 제거</BtnBlackText>}
    </LeftAreaStyle>
  );
};

export default MyPage;

 

개선!!

등록 버튼을 눌렀을 때 실행되는 handleUpload 로직을 fileSelect로 옮겼다.

 

 

[댓글]

댓글 영역

기존에는 댓글을 작성할 때 댓글 데이터에 유저 닉네임도 같이 저장해서 불러왔다. 

그런데 이렇게 하다 보니 유저가 닉네임을 바꿨을 때 같이 업데이트되지 않고 이전에 등록한 댓글에 변경 전 닉네임도 같이 남아 있게 되었다. 

 

나는 이 부분이 굉장히 신경이 쓰였다. 이걸 어떻게 해야 할지 고민하다가 튜터님께 문의를 드렸다. 

서비스마다 다르고 요구사항마다 다르겠지만, 변경된 닉네임으로 보여주고 싶다면 데이터를 전부 업데이트하거나, 닉네임을 유저의 정보에서 가져와서 보여주는 게 맞다고 하셨다.

 

선택한 방법

나는 댓글에 저장된 닉네임을 사용하지 않고, 유저 정보에서 불러와서 보여주기로 결정했다.

 

CommentList 컴포넌트에서 하위 컴포넌트인 CommentItem 컴포넌트로 내려줄 때 댓글 작성한 userId와 같은 id를 가진 유저 정보를 조회하여 그 데이터를 같이 prop으로 내려줬다. 

 

ComppentList.jsx

import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
import { collection, getDocs, query } from 'firebase/firestore';
import { db } from '../../firebase';
import CommentItem from './CommentItem';

export default function CommentList({ commentData, setCommentData }) {
  const [userData, setUserData] = useState();

  //유저 정보 데이터 가져오기
  useEffect(() => {
    const fetchUserData = async () => {
      const q = query(collection(db, 'usersDB'));
      const querySnapshot = await getDocs(q);

      const initialData = [];

      querySnapshot.forEach((doc) => {
        initialData.push({ id: doc.id, ...doc.data() });
      });

      // firestore에서 가져온 데이터를 state에 전달
      setUserData(initialData);
    };

    fetchUserData();
  }, []);

  return (
    <CommentListStyle>
      {commentData &&
        commentData.map((el) => {
          let findData = null;
          //유저 데이터에서 댓글 작성자 id와 같은 데이터 조회
          if (userData) {
            findData = userData.find((item) => {
              return item.user_id === el.user_id;
            });
          }
          return (
            <CommentItem
              key={el.id}
              data={el}
              findData={findData}
              commentData={commentData}
              setCommentData={setCommentData}
            ></CommentItem>
          );
        })}
    </CommentListStyle>
  );
}

const CommentListStyle = styled.ul`
  margin: 0 auto 4rem;
  max-width: 1000px;
`;

 

CommentItem.jsx

import { deleteDoc, doc, updateDoc } from '@firebase/firestore';
import React, { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import styled from 'styled-components';
import { BtnBlackBg, BtnBlackText } from 'styles/SharedStyle';
import { db } from '../../firebase';

export default function CommentItem({ data, findData, commentData, setCommentData }) {
  const { id, user_id, nickname, contents, regDate } = data;
  const { user_id: nowUserId } = useSelector((state) => state.user.nowUser);
  const [isEditing, setIsEditing] = useState(false);
  const [content, setContent] = useState(contents);

  // 유저 정보 - 업데이트 되면 반영됨
  const nowNickname = findData && findData.nickname ? findData.nickname : nickname;
  const nowUserImg = findData && findData.user_img ? findData.user_img : DEFAULT_IMAGE;

  //생략....


  return (
    <CommentItemStyle>
      <CommentTopInfo>
        <ThumbBox>
          <img src={nowUserImg} alt={`${nowNickname} 유저 프로필 이미지`} />
        </ThumbBox>
        <div>
          <NicknameStyle>{nowNickname}</NicknameStyle>
          <DateStyle>{regDate}</DateStyle>
        </div>
      </CommentTopInfo>
      <CommentCont>
        <ContArea>
          {isEditing ? (
            <CommentInput type="text" value={content} onChange={handleChange} placeholder="내용을 입력해주세요" />
          ) : (
            <p>{content}</p>
          )}
        </ContArea>
        {nowUserId === user_id ? (
          !isEditing ? (
            <div>
              <BtnBlackBg onClick={handleEditClick}>수정</BtnBlackBg>
              <BtnBlackText onClick={handleDelete}>삭제</BtnBlackText>
            </div>
          ) : (
            <div>
              <BtnBlackBg onClick={handleSave}>완료</BtnBlackBg>
              <BtnBlackText onClick={handleCancelClick}>취소</BtnBlackText>
            </div>
          )
        ) : (
          <></>
        )}
      </CommentCont>
    </CommentItemStyle>
  );
}

 

부모 컴포넌트에서 내려준 findData에서 닉네임과 유저 프로필 이미지 각각 nowNickname, nowUserImg 변수에 담아서 랜더링 하였다.

이렇게 하니 유저가 정보를 업데이트해도 댓글에 같이 업데이트된 유저 정보를 보여줄 수 있게 되었다.

 

🔥 배운 점 / 느낀 점

그동안 편하게 사용했던 사소한 기능도 이렇게 많은 고민과 시행착오로 만들어지는 것 같다는 생각이 들었다. 

특히 당연히 유저 정보 업데이트하면 댓글에 있는 정보도 같이 업데이트된 정보로 보인다거나 하는...

개발하면서 정말 당연하다는 건 없는 기분이다... 당연하게 느낀 건 그만큼 다른 사람들이 잘 만들어줘서 편하게 사용했구나 하는 생각도 들었다.

 

ux를 잘 챙겨야 할 것 같다.

vercel

vercel 이 팀 계정이 아니면 팀원들이 pr 올리거나 하는 거는 배포가 안된다는 사실을 처음 알게 됐다...

계속 배포가 되다 말다 해서 왜 그런가 했더니 이 이유였다....

팀 계정은 비용을 내고 써야 한다고 한다.

 

발표

이번에 나는 처음으로 발표를 맡게되었다. 하지만 발표 시작 10분 전에 firebase 사용량이 100%를 찍으며, 폭파했고... 나는 발표에서 시연을 할 수 없게 되었다. 

나는 정말 그 짧은 시간동안 멘탈이 나갔다.... 이런 일이 생길 줄이야... 

팀원들 전부 새벽까지 열심히 만들었는데 너무 아쉬웠다.

발표를 마치고 우리 팀은 새로운 firebase 키로 다시 배포를 하며 아쉬움을 달랬다.

 

이번 프로젝트를 하면서 기능을 만들면서 공부한 점들도 많이 배웠지만, 협업이나 발표 직전에 생긴 문제를 겪으면서 더 많이 배운 것 같다. 

(우리팀 진짜 다들 너무 고생하셨고 너무 감사했습니다ㅠㅠㅠ)

 

❕ 아쉬운 점

아직도 개선해야 할 사항이나 에러사항이 눈에 보여서 많이 아쉽다. 

조금 더 좋은 퀄리티로 만들었어야 했는데 하는 아쉬움이 있다. 

그리고 디자인, ui/ux를 더 챙기지 못했던 점도 있다. 

 

남은 개선사항을 정리해 보았다.

 

[마이페이지]

  • 마이페이지에서 이미지 삭제를 누르면 기본 이미지로 변경하는데, 기본 이미지로 업데이트하기 때문에 이미지 제거 버튼이 계속 나오는 에러가 있다.
  • 닉네임 변경할 때 중복된 닉네임이 있는지 확인이 필요할 것 같다.