6 분 소요

1. 프로젝트 소개

소개

  • 프로젝트 클론 사이트 : https://pedia.watcha.com/ko-KR 한국의 영화 추천 서비스로 사용자가 별점, 코멘트를 남길 수 있고 취향을 분석해 좋아할만한 영화를 추천해준다.
  • 진행기간 : 2022년 3월 28일 ~ 2022년 4월8일 (12일)
  • 프로젝트 참여자 : 김영서, 공민지, 서채원, 임경훈, 이의택
  • FRONTEND : React, Router, Sass, Restful API
  • BACKEND : Node.js, Express, Prisma , MySQL , Bcrypt , JWT, cors

시연 영상

아래는 2주간 완성한 프로젝트 시연 영상입니다.

2. 구현 기능

FRONTEND

메인 페이지

  • Nav 바 구현 로그인 (로그인 시 회원 이름이 nav 바에 출력) , 회원가입 , 검색기능
  • 캐러셀 슬라이드 구현
  • 컬렉션 섹션 구현
  • Footer 구현 지금까지의 총 평가 수가 실시간 반영

상세 페이지

  • 평점(마우스가 올라가면 별의 색이 변함), 보고싶어요, 코멘트 창(모달창) 구현
  • 광고, 갤러리(사진을 누르면 커지고 버튼을 누르면 옆 이미지로 이동) 배너 구현
  • 비슷한 작품(장르가 같은 작품) , 영화에 작성된 코멘트 전체보기

검색 페이지

  • 검색 페이지 구현

BACKEND

  • Ratings API (평점을 저장하고, 불러오고 , 전체 평점의 평균을 구하는 API)
  • Want-To API (사용자가 보고싶어요를 클릭하면 저장하는 API)
  • Footer API (전체 사용자가 남긴 평점의 수를 나타내는 API)
  • Sign-in, Sign-up API (로그인, 회원가입 API)
  • Similar movie API (같은 장르의 영화를 보여주는 API)
  • Carousel API (카테고리의 영화를 보여주는 API)
  • Comment API (코멘트를 저장, 삭제, 수정, 입력한 코멘트 불러오기, 코멘트 좋아요 API)

3. 모델링

image-20220411142714800

  • movies 테이블을 중점으로
  • movie의 장르를 관리하는 movies_genre 테이블
  • movie의 나라를 관리하는 movies_country 테이블
  • movie의 이미지를 관리하는 movies_images 테이블
  • movie의 카테고리를 관리하기 위해 categories 테이블과 N:N 관계로 연결하고 사이 테이블로 Movies_categories 테이블이 있다.
  • 유저의 정보를 입력하는 users 테이블이 있고
  • 평점을 저장하기 위해 ratings(평점을 준 유저와 평점을 받은 영화를 N:N 관계로 연결) 테이블, 코멘트를 저장하기 위해 comments(코멘트를 남긴 영화와 유저의 정보를 N:N 관계로 연결) 테이블
  • 유저가 해당 코멘트에 좋아요를 눌렀는지 확인하는 users_comments_likes (해당 유저가 어떤 코멘트에 좋아요를 눌렀는지 N:N 관계로 연결) 테이블
  • 유저가 해당 영화를 보고 싶은지 관리하는 wants(어떤 영화를 누가 보고 싶은지 N:N 으로 연결) 테이블로 구성되어 있다.

4. 기록하고 싶은 코드

프론트엔드

ezgif.com-gif-maker

  const addSearchWord = item => {
    const items = {
      id: arrayKey,
      item: item,
    };
    arrayKey += 1;
    setsearchWord([items, ...searchWord]);
    goToSearchPage(items.item);     //문제 발견
  };

문제점 발견 후 아래 코드로 변경

  const addSearchWord = item => {
    const items = {
      id: arrayKey,
      item: item,
    };
    arrayKey += 1;
    let newSearchword = searchWord;
    newSearchword.unshift(items);
    setsearchWord(newSearchword);
    window.localStorage.setItem('item', JSON.stringify(searchWord)); //추가
    goToSearchPage(items.item);
  };

  const goToSearchPage = item => {
    navigate(`/search?${item}`);
    window.location.reload();
  };

  useEffect(() => {
    window.localStorage.setItem('item', JSON.stringify(searchWord));
  }, [searchWord]);

검색을 하면 검색어를 로컬 스토리지에 저장해 최근 검색어 목록을 보여주는 기능을 구현했다.

처음 코드는 검색어를 입력하면 그 검색어가 item으로 들어오고 item에 id값을 넣어준 뒤 setsearchWord를 통해서 useState에 저장되면서 searchWord에 변화가 생겼기 때문에 useEffect를 통해 localStorage에 저장해주는 코드를 구현했다.

하지만 window.location.reload를 통해서 페이지를 새로고침 시켰다. useEffect의 콜백은 렌더링 이후에 호출되기 때문에 setsearchWord 후 새로고침이 되기 때문에 setsearchWord 렌더링은 되지 않는다.

검색 페이지에서 검색시 로컬 스토리지에 저장되었던 이유는 같은 주소의 url 이기 때문에 되었다. 같은 코드로 똑같은 동작을 하는데 메인 페이지에서 검색을 하면 저장되지 않고 검색페이지에서 검색시 저장이되어 큰 혼란을 주었던 코드이다.

렌더링 되지 않기 때문에 useEffect 안에 있던 코드를 직접 넣어주어 해결 했다.

ezgif.com-gif-maker (1)

갤러리 배너 구현

function Gallery(props) {

  const setCursor = event => {
    setScrollX(event.target.scrollLeft);
    setScrollEnd(event.target.scrollWidth - event.target.clientWidth);
  };

  const moveLeft = scrollOffset => {
    Section.current.scrollLeft += scrollOffset;
  };

  const modal = imgUrl => {
    if (imgUrl) {
      document.body.style.overflow = 'hidden';
      setImgUrls(imgUrl[0]);
      setIsModalOpen(true);
      setImgIndex(imgUrl[1]);
    }
  };
  let totalimageindex = imgIndex;
  const modalFalse = e => {
    document.body.style.overflow = 'unset';
    setIsModalOpen(false);
  };
  const notClick = event => {
    event.stopPropagation();
  };
  const LeftPage = event => {
    event.stopPropagation();
    setImgIndex(imgIndex - 1);
    totalimageindex -= 1;
    setImgUrls('http://' + images[totalimageindex].images_url);
  };
  const RightPage = event => {
    event.stopPropagation();
    setImgIndex(imgIndex + 1);
    totalimageindex = totalimageindex + 1;
    setImgUrls('http://' + images[totalimageindex].images_url);
  };
  useEffect(() => {
    totalimageindex === 0 ? setLeftShow(true) : setLeftShow(false);
    totalimageindex === images.length - 1
      ? setRightShow(true)
      : setRightShow(false);
  }, [imgIndex]);
  useEffect(() => {
    fetch(`/movie/images/${props.movieId}`, {
      method: 'GET',
    })
      .then(res => res.json())
      .then(data => {
        setImages(data.movieImages);
      });
  }, [props.movieId]);

이미지 슬라이더 기능을 통해서 캐러셀 슬라이드 기능도 구현했고 이미지를 누르면 모달창을 이용해서 화면에 크게 사진이 뜨고 사진 양옆에 버튼을 통해서 이동도 가능하며 첫 장과 끝장에서는 더 이상 옆으로 넘어가지 못하게 버튼을 숨겨주는 기능을 만들었다.

이 기능을 통해서 버튼을 누르면 사진의 url에 변화를 시켜서 다음 사진을 보여주도록 구현을 했는데 모달창 위에 버튼이 있고 사진을 제외한 다른 부분의 모달창을 클릭하게 되면 모달창이 꺼지게 해야 하기 때문에 중복된 부분을 처리하는 게 어려웠다.

버튼을 클릭 시 url의 정보가 계속 한 박자씩 늦게 바뀌는 오류가 발생하였고 그 부분을 useEffect를 이용해서 해결했다. 이번 이미지 슬라이더를 통해 useEffect를 사용하는 방법에 대해 확실하게 이해할 수 있는 시간이었다.

백엔드

COMMENT API 구현

const express = require("express");
const router = express.Router();

const CommentController = require("../controllers/CommentController");
const authorMiddleware = require("../controllers/middlewares/Authorization");

router.get("/content", authorMiddleware.Authorization, CommentController.CommentSelect);
router.get("/", CommentController.CommentList);
router.post("/", authorMiddleware.Authorization, CommentController.CommentAdd);
router.put("/", authorMiddleware.Authorization, CommentController.CommentModify);
router.delete("/", authorMiddleware.Authorization, CommentController.CommentDelete);

router.post("/like", authorMiddleware.Authorization, CommentController.CommentLikePush);
router.delete("/like", authorMiddleware.Authorization, CommentController.CommentLikeDelete);
router.get("/like", CommentController.CommentLikeGet);
router.get("/like/check",  authorMiddleware.Authorization,CommentController.CommentLikeCheck);
router.use("/", CommentController.error);

module.exports = router;

최대한 restful API 를 작성하기 위해서 노력했다.

에러와 Authorization 부분을 미들웨어로 만들어서 반복되는 작업을 처리해줬다.

//CommentService.js

const CommentList = async (movieId, res) => {
  const CommentList = await CommontDao.CommentList(movieId);
  const CommentRating = await CommontDao.commentRating(movieId);

  const result = CommentList.map((value, index) => {
    return Object.assign(value, CommentRating[index]);
  });

  return result;
};

map 을 이용해서 하나의 데이터로 만들었다.

image-20220411164904855

//CommentDao.js

const CommentList = async (movieId) => {
  const CommentList = await prisma.$queryRaw`
  SELECT A.id AS comment_id, C.id ,C.name , A.comment ,(SELECT count(B.comment_id) from users_comments_likes B where comment_id = A.id group by B.comment_id) AS count 
        FROM 
        comments A 
        LEFT JOIN  users_comments_likes B on A.user_id = B.user_id
        LEFT JOIN users C on A.user_id = C.id
        WHERE A.movie_id = ${movieId}
        GROUP BY  A.comment , A.id;`;

  return CommentList;
};

const commentRating = async (movieId) => {
  const CommentList = await prisma.$queryRaw`
  SELECT A.id , B.count , C.want 
        FROM 
        users A 
        LEFT JOIN  ratings B on A.id = B.user_id
        LEFT join wants C on A.id = C.user_id
        RIGHT JOIN comments D on A.id  = D.user_id 
        WHERE C.movie_id is null or C.movie_id = ${movieId}
        GROUP BY A.id , B.count, B.movie_id , C.want
        HAVING movie_id is null or movie_id = ${movieId} 
`;
  return CommentList;
};

많은 양의 데이터를 한 번의 쿼리문을 사용하여 데이터를 가져오려 노력했지만 실패했다.

그래서 두번의 쿼리문을 이용하고 하나로 붙여서 return 해주는 코드를 구현했다.

5. 회고

프로젝트를 통해서 느낀점을 적어보려고 한다.

1. 의사소통이 중요하다.

프로젝트를 시작하기 전에 개발자에게 커뮤니케이션 스킬이 중요하다는 얘기는 많이 들었지만 정확히 어떤 부분에서 중요하고 왜 중요하다고 말하는지 많이 느끼지는 못했다. 하지만 이번 협업을 해보면서 커뮤니케이션이 정말 중요하다고 생각하게 되는 좋은 시간이었다.

팀원들과 미팅을 하면서 가장 어려웠던 부분은 상대방의 말을 이해하는 것이었다. 이해를 못 해서 계속 제가 이해한 말은 이게 맞는데 이렇게 설명하신 것이 맞냐고 물어봤다… 상대방의 말을 정확하게 이해하기는 어렵고 나의 의견을 상대방에게 정확히 말하는 것도 상당히 어려운 시간이었다. 다들 생각이 다르기 때문에 내가 아무리 A라고 설명해도 상대방은 B라고 알아들을 수 있다. 그래서 최대한 집중해 상대방의 말을 한 번에 이해하려고 노력했고 그 사람의 성향도 어느 정도는 파악하는 게 도움이 많이 되었다. 말을 할 때도 상대방의 입장에서 말하는 게 중요하다고 다시 한번 느꼈다. 한 번의 미팅이 끝나면 머리가 아플 정도로 생각을 많이 했다. 서로 의견의 충돌이 생기면 맞춰가는 과정도 쉽지 않았다. 최선의 방법을 선택하고 잘못된 방향으로 가지 않기 위해 노력한 거 같다. 그 부분에서 모두 서로의 의견을 묻고 정리해 잘 선택했다.

지금도 의사소통이 이렇게 중요한데 나중에 현업에 나가면 개발자 말고 다른 분야의 사람들과도 계속 소통을 하고 중요성은 점점 커질 것이다. 커뮤니케이션 스킬 향상을 위해 노력할 것이며 2차 프로젝트에서는 조금 더 잘할 수 있을 거 같다.

2. 모두가 이해할 수 있는 코드를 짜야한다.

내가 친 코드를 고치는 것보다 남의 코드를 고치는 작업은 매우 힘들었다. 나 말고 다른 팀원이 내 코드를 봐도 똑같이 느꼈을 것이다. 가독성이 좋고 이해하기 쉬운 코드를 짜야 한다. 아무리 기능별, 컴포넌트 별로 역할을 나눠서 개발을 해도 나의 코드를 다른 사람에게 설명할 시간이 생기고 다른 사람의 코드를 내가 봐줄 시간이 생긴다. 가독성 좋고 명확한 코드의 중요성은 잘 알고 있었지만 앞으로 더 좋은 코드를 짜기 위해 노력해야겠다.

3. 여러 컴포넌트 하나로

메인 페이지를 하나로 합치는 작업을 했다. 금방 할 수 있을 거 같던 작업이었지만 실제로 해보니 많은 시간이 필요 했다. 각자 다른 컴포넌트를 통일성 있게 만들고 맞는 자리에 넣어주고 길이를 맞춰줘야 했다. 다음 프로젝트에서는 처음부터 통일성을 가지고 개발해야 겠다고 생각했다. 또한, 무조건 목 데이터를 만들어서 작업해야겠다. 목 데이터를 만들지 않고 작업하면 나중에 fetch를 이용해서 데이터를 받아오는 작업을 하는데 시간이 많이 걸린다.

4. 프론트 , 백

이번 프로젝트에서는 프론트와 백 두 부분을 모두 개발했다. 모두 개발해 보니까 정확히 어디까지 프론트고 어디까지가 백인지 알 수 있었다. 내가 만약 프론트만 맡아서 개발을 했다면 백에서 데이터를 어떻게 받아와서 처리하고 사용할지 정확한 개념이 잡히지 않았을 수 있었다. 하지만 다음 프로젝트는 내가 좀 더 좋아하는 프론트 개발만 하고 싶다.

카테고리:

업데이트:

댓글남기기