ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -0,0 +1,48 @@
+const bcrypt = require('bcrypt');
+const { userDao } = require('../dao');
+
+const validateEmail = (email) => {
+  const validEmailRegExp = /\w@\w+\.\w/i;
+  return validEmailRegExp.test(email);
+};
+
+const validatePw = (pw) => {
+  const pwValidation = {
+    regexUppercase: /[A-Z]/g,
+    regexLowercase: /[a-z]/g,
+    regexSpecialCharacter: /[!|@|#|$|%|^|&|*]/g,
+    regexDigit: /[0-9]/g,
+  };
+  const MIN_PW_LENGTH = 8;
+  const pwLength = pw.length;
+
+  if (pwLength < MIN_PW_LENGTH) return false;
+
+  for (const validType in pwValidation) {
+    if (!pwValidation[validType].test(pw)) {
+      return false;
+    }
+  }
+
+  return true;
+};
+
+const hashPassword = async (password) => {
+  return await bcrypt.hash(password, 10);
+};
+
+const findUser = async (fields) => {
+  return await userDao.findUser(fields);
+};
+
+const createUser = async (userData) => {
+  return await userDao.createUser(userData);
+};
+
+module.exports = {
+  validateEmail,
+  validatePw,
+  hashPassword,
+  findUser,
+  createUser,
+};

리뷰: 이런 함수는 아예 모듈화해서 다른 곳에서도 범용적으로 사용해도 좋을 것 같습니다!
@@ -0,0 +1,40 @@
+// Controller는 오직 Service 레이어에만 의존합니다.
+const { userService } = require('../services');
+const { errorGenerator } = require('../erros');
+
+const signUp = async (req, res, next) => {
+  try {
+    const {
+      email,
+      hashedPassword,
+      phoneNumber,
+      profileImageUrl,
+      introduction,
+    } = req.body;
+
+    /*
+      middleware에서 email, hashedPassword, phoneNumber의
+      존재에 대한 에러 처리 했는데,
+      여기서도 또 에러 처리를 해야할까요?
+    */
+
+    const createdUser = await userService.createUser({
+      email,
+      password: hashedPassword,
+      phoneNumber,
+      profileImageUrl,
+      introduction,
+    });
+
+    res.status(201).json({
+      message: 'user created',
+      userId: createdUser.id,
+    });
+  } catch (err) {
+    next(err);
+  }
+};
+
+module.exports = {
+  signUp,
+};

리뷰: 이 부분 작동 잘 하나요? 키 값이 안 맞는 것 같습니다.
@@ -0,0 +1,40 @@
+// Controller는 오직 Service 레이어에만 의존합니다.
+const { userService } = require('../services');
+const { errorGenerator } = require('../erros');
+
+const signUp = async (req, res, next) => {
+  try {
+    const {
+      email,
+      hashedPassword,
+      phoneNumber,
+      profileImageUrl,
+      introduction,
+    } = req.body;
+
+    /*
+      middleware에서 email, hashedPassword, phoneNumber의
+      존재에 대한 에러 처리 했는데,
+      여기서도 또 에러 처리를 해야할까요?
+    */
+
+    const createdUser = await userService.createUser({
+      email,
+      password: hashedPassword,
+      phoneNumber,
+      profileImageUrl,
+      introduction,
+    });
+
+    res.status(201).json({
+      message: 'user created',
+      userId: createdUser.id,
+    });
+  } catch (err) {
+    next(err);
+  }
+};
+
+module.exports = {
+  signUp,
+};

리뷰: 수정했습니다! validation 로직은 예전 제 코드 긁어오다가 놓친것 같습니다.
@@ -0,0 +1,40 @@
+// Controller는 오직 Service 레이어에만 의존합니다.
+const { userService } = require('../services');
+const { errorGenerator } = require('../erros');
+
+const signUp = async (req, res, next) => {
+  try {
+    const {
+      email,
+      hashedPassword,
+      phoneNumber,
+      profileImageUrl,
+      introduction,
+    } = req.body;
+
+    /*
+      middleware에서 email, hashedPassword, phoneNumber의
+      존재에 대한 에러 처리 했는데,
+      여기서도 또 에러 처리를 해야할까요?
+    */
+
+    const createdUser = await userService.createUser({
+      email,
+      password: hashedPassword,
+      phoneNumber,
+      profileImageUrl,
+      introduction,
+    });
+
+    res.status(201).json({
+      message: 'user created',
+      userId: createdUser.id,
+    });
+  } catch (err) {
+    next(err);
+  }
+};
+
+module.exports = {
+  signUp,
+};

리뷰: 프론트단에서 받은 `req.body`에 위 키값들이 누락될 경우 그 값이 undefined로 되는 것 같은데,
제가 놓치고 있는 부분이 있을까요?
@@ -0,0 +1,40 @@
+// Controller는 오직 Service 레이어에만 의존합니다.
+const { userService } = require('../services');
+const { errorGenerator } = require('../erros');
+
+const signUp = async (req, res, next) => {
+  try {
+    const {
+      email,
+      hashedPassword,
+      phoneNumber,
+      profileImageUrl,
+      introduction,
+    } = req.body;
+
+    /*
+      middleware에서 email, hashedPassword, phoneNumber의
+      존재에 대한 에러 처리 했는데,
+      여기서도 또 에러 처리를 해야할까요?
+    */
+
+    const createdUser = await userService.createUser({
+      email,
+      password: hashedPassword,
+      phoneNumber,
+      profileImageUrl,
+      introduction,
+    });
+
+    res.status(201).json({
+      message: 'user created',
+      userId: createdUser.id,
+    });
+  } catch (err) {
+    next(err);
+  }
+};
+
+module.exports = {
+  signUp,
+};

리뷰: `RegExp.prototype.test()`로 수정했습니다!
@@ -0,0 +1,40 @@
+// Controller는 오직 Service 레이어에만 의존합니다.
+const { userService } = require('../services');
+const { errorGenerator } = require('../erros');
+
+const signUp = async (req, res, next) => {
+  try {
+    const {
+      email,
+      hashedPassword,
+      phoneNumber,
+      profileImageUrl,
+      introduction,
+    } = req.body;
+
+    /*
+      middleware에서 email, hashedPassword, phoneNumber의
+      존재에 대한 에러 처리 했는데,
+      여기서도 또 에러 처리를 해야할까요?
+    */
+
+    const createdUser = await userService.createUser({
+      email,
+      password: hashedPassword,
+      phoneNumber,
+      profileImageUrl,
+      introduction,
+    });
+
+    res.status(201).json({
+      message: 'user created',
+      userId: createdUser.id,
+    });
+  } catch (err) {
+    next(err);
+  }
+};
+
+module.exports = {
+  signUp,
+};

리뷰: 감사합니다!
@@ -0,0 +1,40 @@
+// Controller는 오직 Service 레이어에만 의존합니다.
+const { userService } = require('../services');
+const { errorGenerator } = require('../erros');
+
+const signUp = async (req, res, next) => {
+  try {
+    const {
+      email,
+      hashedPassword,
+      phoneNumber,
+      profileImageUrl,
+      introduction,
+    } = req.body;
+
+    /*
+      middleware에서 email, hashedPassword, phoneNumber의
+      존재에 대한 에러 처리 했는데,
+      여기서도 또 에러 처리를 해야할까요?
+    */
+
+    const createdUser = await userService.createUser({
+      email,
+      password: hashedPassword,
+      phoneNumber,
+      profileImageUrl,
+      introduction,
+    });
+
+    res.status(201).json({
+      message: 'user created',
+      userId: createdUser.id,
+    });
+  } catch (err) {
+    next(err);
+  }
+};
+
+module.exports = {
+  signUp,
+};

리뷰: 월요일에 말씀해주신대로 `return res` 형태가 아니라 에러 핸들링을 모듈화 해보겠습니다 :)
@@ -0,0 +1,40 @@
+// Controller는 오직 Service 레이어에만 의존합니다.
+const { userService } = require('../services');
+const { errorGenerator } = require('../erros');
+
+const signUp = async (req, res, next) => {
+  try {
+    const {
+      email,
+      hashedPassword,
+      phoneNumber,
+      profileImageUrl,
+      introduction,
+    } = req.body;
+
+    /*
+      middleware에서 email, hashedPassword, phoneNumber의
+      존재에 대한 에러 처리 했는데,
+      여기서도 또 에러 처리를 해야할까요?
+    */
+
+    const createdUser = await userService.createUser({
+      email,
+      password: hashedPassword,
+      phoneNumber,
+      profileImageUrl,
+      introduction,
+    });
+
+    res.status(201).json({
+      message: 'user created',
+      userId: createdUser.id,
+    });
+  } catch (err) {
+    next(err);
+  }
+};
+
+module.exports = {
+  signUp,
+};

리뷰: 감사합니다!!!!!!
@@ -0,0 +1,40 @@
+// Controller는 오직 Service 레이어에만 의존합니다.
+const { userService } = require('../services');
+const { errorGenerator } = require('../erros');
+
+const signUp = async (req, res, next) => {
+  try {
+    const {
+      email,
+      hashedPassword,
+      phoneNumber,
+      profileImageUrl,
+      introduction,
+    } = req.body;
+
+    /*
+      middleware에서 email, hashedPassword, phoneNumber의
+      존재에 대한 에러 처리 했는데,
+      여기서도 또 에러 처리를 해야할까요?
+    */
+
+    const createdUser = await userService.createUser({
+      email,
+      password: hashedPassword,
+      phoneNumber,
+      profileImageUrl,
+      introduction,
+    });
+
+    res.status(201).json({
+      message: 'user created',
+      userId: createdUser.id,
+    });
+  } catch (err) {
+    next(err);
+  }
+};
+
+module.exports = {
+  signUp,
+};

리뷰: 에러 핸들링뿐만 아니라 응답도 모듈화 해보겠습니다 :)
@@ -0,0 +1,13 @@
+const express = require('express');
+const router = express.Router();
+const middleware = require('../middlewares');
+const { userController } = require('../controllers');
+// Route 는 오직 Controller 에만 의존 합니다.
+
+router.post(
+  '/signup',
+  [middleware.validateSignUpUserData, middleware.hashPassword],
+  userController.signUp,
+);
+
+module.exports = router;

리뷰: convention 수정하려다가 놓친것 같습니다... 관련된 부분 모두 수정하고 회원가입도 확인했습니다!
@@ -0,0 +1,40 @@
+// Controller는 오직 Service 레이어에만 의존합니다.
+const { userService } = require('../services');
+const { errorGenerator } = require('../erros');
+
+const signUp = async (req, res, next) => {
+  try {
+    const {
+      email,
+      hashedPassword,
+      phoneNumber,
+      profileImageUrl,
+      introduction,
+    } = req.body;
+
+    /*
+      middleware에서 email, hashedPassword, phoneNumber의
+      존재에 대한 에러 처리 했는데,
+      여기서도 또 에러 처리를 해야할까요?
+    */
+
+    const createdUser = await userService.createUser({
+      email,
+      password: hashedPassword,
+      phoneNumber,
+      profileImageUrl,
+      introduction,
+    });
+
+    res.status(201).json({
+      message: 'user created',
+      userId: createdUser.id,
+    });
+  } catch (err) {
+    next(err);
+  }
+};
+
+module.exports = {
+  signUp,
+};

리뷰: dao와 services로 분리하기로 결정했습니다.
@@ -0,0 +1,40 @@
+// Controller는 오직 Service 레이어에만 의존합니다.
+const { userService } = require('../services');
+const { errorGenerator } = require('../erros');
+
+const signUp = async (req, res, next) => {
+  try {
+    const {
+      email,
+      hashedPassword,
+      phoneNumber,
+      profileImageUrl,
+      introduction,
+    } = req.body;
+
+    /*
+      middleware에서 email, hashedPassword, phoneNumber의
+      존재에 대한 에러 처리 했는데,
+      여기서도 또 에러 처리를 해야할까요?
+    */
+
+    const createdUser = await userService.createUser({
+      email,
+      password: hashedPassword,
+      phoneNumber,
+      profileImageUrl,
+      introduction,
+    });
+
+    res.status(201).json({
+      message: 'user created',
+      userId: createdUser.id,
+    });
+  } catch (err) {
+    next(err);
+  }
+};
+
+module.exports = {
+  signUp,
+};

리뷰: 제가 착각했네요! 감사합니다 :)
@@ -0,0 +1,20 @@
+const { errorGenerator } = require('../erros');
+const { userService } = require('../services');
+
+const hashPassword = async (req, res, next) => {
+  try {
+    const { password } = req.body;
+
+    !password && errorGenerator(400);
+
+    req.body.hashedPassword = await userService.hashPassword(password);
+
+    next();
+  } catch (err) {
+    next(err);
+  }
+};
+
+module.exports = {
+  hashPassword,
+};

리뷰: 미들웨어에서 비밀번호 암호화를 하는 건가요? 이 방법도 굉장히 참신한 것 같습니다. 
만약 서비스가 커지게 되거나, 모노리포를 적용한다거나, 기타 다양한 방식으로 비밀번호 암호화를 활용하게 될 경우가 있기 때문에 보통 util 함수로 빼내어 다른 곳에서도 사용할 수 있도록 공통 함수로 만드는 것이 조금 더 보편적이기는 한 것 같습니다.
@@ -0,0 +1,31 @@
+const { errorGenerator } = require('../erros');
+const { userService } = require('../services');
+
+const validateSignUpUserData = async (req, res, next) => {
+  try {
+    const { email, password, phoneNumber, profileImageUrl, introduction } =
+      req.body;
+
+    const REQUIRED_INFO = { email, password, phoneNumber };
+
+    for (const info in REQUIRED_INFO) {
+      !REQUIRED_INFO[info] && errorGenerator(400, `MISSING ${info}`);
+    }
+
+    !userService.validateEmail(email) && errorGenerator(400, 'INVALID EMAIL');
+
+    const foundUser = await userService.findUser({ email });
+    foundUser && errorGenerator(409);
+
+    !userService.validatePw(password) &&
+      errorGenerator(400, 'INVALID PASSWORD');
+
+    next();
+  } catch (err) {
+    next(err);
+  }
+};
+
+module.exports = {
+  validateSignUpUserData,
+};

리뷰: 다른 POST 에서도 필수적인 키값들을 확인할 일이 많지 않을까요? 
범용적으로 활용할 수 있는 + 필수 body 키 값을 확인하는 공통 함수가 있어도 좋을 것 같습니다.
@@ -0,0 +1,48 @@
+const bcrypt = require('bcrypt');
+const { userDao } = require('../dao');
+
+const validateEmail = (email) => {
+  const validEmailRegExp = /\w@\w+\.\w/i;
+  return validEmailRegExp.test(email);
+};
+
+const validatePw = (pw) => {
+  const pwValidation = {
+    regexUppercase: /[A-Z]/g,
+    regexLowercase: /[a-z]/g,
+    regexSpecialCharacter: /[!|@|#|$|%|^|&|*]/g,
+    regexDigit: /[0-9]/g,
+  };
+  const MIN_PW_LENGTH = 8;
+  const pwLength = pw.length;
+
+  if (pwLength < MIN_PW_LENGTH) return false;
+
+  for (const validType in pwValidation) {
+    if (!pwValidation[validType].test(pw)) {
+      return false;
+    }
+  }
+
+  return true;
+};
+
+const hashPassword = async (password) => {
+  return await bcrypt.hash(password, 10);
+};
+
+const findUser = async (fields) => {
+  return await userDao.findUser(fields);
+};
+
+const createUser = async (userData) => {
+  return await userDao.createUser(userData);
+};
+
+module.exports = {
+  validateEmail,
+  validatePw,
+  hashPassword,
+  findUser,
+  createUser,
+};

리뷰: Regex 객체를 활용해서 조금 더 간단하게 작성하는 것은 어떤가요? Regex.prototype.test 자체가 boolean 을 반환하게 되니까요!
+test 메서드가 match 보다 성능상 우위에 있기도 합니다!
@@ -0,0 +1,20 @@
+const { errorGenerator } = require('../erros');
+const { userService } = require('../services');
+
+const hashPassword = async (req, res, next) => {
+  try {
+    const { password } = req.body;
+
+    !password && errorGenerator(400);
+
+    req.body.hashedPassword = await userService.hashPassword(password);
+
+    next();
+  } catch (err) {
+    next(err);
+  }
+};
+
+module.exports = {
+  hashPassword,
+};

리뷰: 감사합니다!
@@ -0,0 +1,31 @@
+const { errorGenerator } = require('../erros');
+const { userService } = require('../services');
+
+const validateSignUpUserData = async (req, res, next) => {
+  try {
+    const { email, password, phoneNumber, profileImageUrl, introduction } =
+      req.body;
+
+    const REQUIRED_INFO = { email, password, phoneNumber };
+
+    for (const info in REQUIRED_INFO) {
+      !REQUIRED_INFO[info] && errorGenerator(400, `MISSING ${info}`);
+    }
+
+    !userService.validateEmail(email) && errorGenerator(400, 'INVALID EMAIL');
+
+    const foundUser = await userService.findUser({ email });
+    foundUser && errorGenerator(409);
+
+    !userService.validatePw(password) &&
+      errorGenerator(400, 'INVALID PASSWORD');
+
+    next();
+  } catch (err) {
+    next(err);
+  }
+};
+
+module.exports = {
+  validateSignUpUserData,
+};

리뷰: 오...! 매우 유용한 함수가 될 것 같습니다!
@@ -0,0 +1,31 @@
+const { errorGenerator } = require('../erros');
+const { userService } = require('../services');
+
+const validateSignUpUserData = async (req, res, next) => {
+  try {
+    const { email, password, phoneNumber, profileImageUrl, introduction } =
+      req.body;
+
+    const REQUIRED_INFO = { email, password, phoneNumber };
+
+    for (const info in REQUIRED_INFO) {
+      !REQUIRED_INFO[info] && errorGenerator(400, `MISSING ${info}`);
+    }
+
+    !userService.validateEmail(email) && errorGenerator(400, 'INVALID EMAIL');
+
+    const foundUser = await userService.findUser({ email });
+    foundUser && errorGenerator(409);
+
+    !userService.validatePw(password) &&
+      errorGenerator(400, 'INVALID PASSWORD');
+
+    next();
+  } catch (err) {
+    next(err);
+  }
+};
+
+module.exports = {
+  validateSignUpUserData,
+};

리뷰: 의견 감사합니다 :)
@@ -0,0 +1,93 @@
+const context = describe;
+
+describe('영화목록 API 테스트', () => {
+  context('인기 영화목록 1페이지를 조회하면', () => {
+    it('20개의 목록을 반환한다.', () => {
+      const baseUrl = 'https://api.themoviedb.org/3/movie/popular';
+      const param = new URLSearchParams({
+        api_key: Cypress.env('API_KEY'),
+        language: 'ko-KR',
+        page: 1,
+      });
+
+      cy.request('GET', `${baseUrl}?${param}`).as('popularMovies');
+
+      cy.get('@popularMovies').its('status').should('eq', 200);
+      cy.get('@popularMovies').its('body.results').should('have.length', 20);
+    });
+  });
+});
+
+describe('영화목록 e2e 테스트', () => {
+  beforeEach(() => {
+    const baseUrl = 'https://api.themoviedb.org/3/movie/popular';
+    const param = new URLSearchParams({
+      api_key: Cypress.env('API_KEY'),
+      language: 'ko-KR',
+      page: 1,
+    });
+
+    cy.intercept(
+      {
+        method: 'GET',
+        url: `${baseUrl}?${param}`,
+      },
+      { fixture: 'movie-popular.json' }
+    ).as('popularMovies');
+
+    cy.visit('http://localhost:8080');
+  });
+
+  context('영화 목록 조회 시', () => {
+    it('800ms 동안 Skeleton UI가 노출되었다가 사라진다.', () => {
+      cy.wait('@popularMovies');
+      cy.get('.skeleton').should('have.length', 60);
+      cy.wait(800);
+      cy.get('.skeleton').should('have.length', 0);
+    });
+  });
+
+  context('페이지 진입 시 인기영화 목록 1페이지를 조회하면', () => {
+    it('20개의 .item-card가 노출된다.', () => {
+      cy.get('.item-card').should('have.length', 20);
+    });
+  });
+
+  context('더보기 버튼을 클릭하면', () => {
+    it('40개의 item-card 노출된다.', () => {
+      cy.get('.btn').click();
+      cy.get('.item-card').should('have.length', 40);
+    });
+  });
+});
+
+describe('짧은 영화목록 e2e 테스트', () => {
+  beforeEach(() => {
+    const baseUrl = 'https://api.themoviedb.org/3/movie/popular';
+    const param = new URLSearchParams({
+      api_key: Cypress.env('API_KEY'),
+      language: 'ko-KR',
+      page: 1,
+    });
+
+    cy.intercept(
+      {
+        method: 'GET',
+        url: `${baseUrl}?${param}`,
+      },
+      { fixture: 'movie-popular-short.json' }
+    ).as('popularMovies');
+
+    cy.visit('http://localhost:8080');
+  });
+
+  context('total_pages 가 1인 인기영화 목록 1페이지를 조회하면', () => {
+    it('18개의 .item-card가 노출된다.', () => {
+      cy.get('.item-card').should('have.length', 18);
+    });
+
+    it('더 보기 버튼이 노출되지 않는다.', () => {
+      cy.contains('더보기').should('not.exist');
+    });
+  });
+});

리뷰: 혹시 18개인 이유가 있을까요?
@@ -0,0 +1,10 @@
+영화 목록 API
+- 인기 영화목록 1페이지를 조회하면 20개의 목록을 반환한다.
+
+페이지
+- 페이지를 증가할 수 있다.
+
+UI
+- 더보기 버튼을 누르면 다음 영화 목록을 불러온다.
+- 영화 목록의 페이지 끝에 도달한 경우에는 더보기 버튼을 화면에 출력하지 않는다.
+- 영화 목록 아이템은 Skeleton UI를 가진다.

리뷰: 페이지를 초기화할 수 있는 기능이 있을까요?
→ 아래의 코멘트들과 목적은 이어집니다!
@@ -0,0 +1,18 @@
+const DEFAULT_SEARCH_PARAMS = {
+  api_key: process.env.API_KEY,
+  language: 'ko-KR',
+};
+const MOVIE_BASE_URL = 'https://api.themoviedb.org/3/movie';
+
+export async function getPopularMovie(page) {
+  const param = new URLSearchParams({
+    ...DEFAULT_SEARCH_PARAMS,
+    page,
+  });
+
+  const response = await fetch(`${MOVIE_BASE_URL}/popular?${param}`);
+  if (response.ok) {
+    return response.json();
+  }
+  return { results: [] };
+}

리뷰: 조금 특이한 방식인 것 같긴한데요! 이렇게 객체로 묶어서 export 하신 이유가 있으신가요?
@@ -0,0 +1,12 @@
+import PageHandler from './PageHandler';
+import { getPopularMovie } from '../api/movie';
+
+export async function getNextPopularMovieList() {
+  const { page, done } = PageHandler.next();
+  const { results } = await getPopularMovie(page);
+  return {
+    page,
+    done,
+    nextMovieList: results,
+  };
+}

리뷰: 아직 구현되지 않은 기능에 대한 함수를 먼저 생성해두신 이유가 있을까요?!
@@ -0,0 +1,12 @@
+import PageHandler from './PageHandler';
+import { getPopularMovie } from '../api/movie';
+
+export async function getNextPopularMovieList() {
+  const { page, done } = PageHandler.next();
+  const { results } = await getPopularMovie(page);
+  return {
+    page,
+    done,
+    nextMovieList: results,
+  };
+}

리뷰: 핸들러 함수에서 UI를 조작해주고 있네요! 처음엔 `if (PageHandler.getCurrentPage() !== total_pages)` 조건이 UI 와 관련이 있다고 생각했는데 각각 목적이 다르네요.

여기서 이 핸들러 함수에 대해서 두 가지를 고민해보면 좋을 것 같아요!

1. `onClickMoreButton` 라는 이름이 해당 함수가 무엇을 하는지 드러낼 수 있는가?
2. UI 조작과 도메인 로직을 함께 두는 게 맞을까?
@@ -0,0 +1,42 @@
+const MOVIE_TOTAL_PAGE_LIMIT = 500;
+
+const INITIAL_VALUE = {
+  page: 1,
+  totalPages: MOVIE_TOTAL_PAGE_LIMIT,
+};
+
+function PageEventHandler() {
+  let attr = { ...INITIAL_VALUE };
+
+  return {
+    next() {
+      if (!this.hasNextPage()) {
+        return {
+          page: attr.page,
+          done: true,
+        };
+      }
+      attr.page = attr.page + 1;
+
+      return {
+        page: attr.page,
+        done: !this.hasNextPage(),
+      };
+    },
+    getCurrentPage() {
+      return attr.page;
+    },
+    setTotalPages(totalPages) {
+      if (totalPages > MOVIE_TOTAL_PAGE_LIMIT) {
+        return;
+      }
+      attr.totalPages = totalPages;
+    },
+    hasNextPage() {
+      return attr.page < attr.totalPages;
+    },
+  };
+}
+
+const PageHandler = PageEventHandler();
+export default PageHandler;

리뷰: 마찬가지로 사용하지 않는 기능들에 대한 구현들이 있는 것 같은데요!
제거하지 않고 남겨 놓으신 이유가 있으실까요?
@@ -0,0 +1,12 @@
+import PageHandler from './PageHandler';
+import { getPopularMovie } from '../api/movie';
+
+export async function getNextPopularMovieList() {
+  const { page, done } = PageHandler.next();
+  const { results } = await getPopularMovie(page);
+  return {
+    page,
+    done,
+    nextMovieList: results,
+  };
+}

리뷰: 조금 더 의도를 전달드리자면!
`onClickMoreButton`은 클릭 + 특정버튼 + 핸들러라는 것만 드러나지,
이 함수가 실제로 어떤 행동을 하는 지는 전혀 알려 주지 못하고 있습니다!
@@ -0,0 +1,93 @@
+const context = describe;
+
+describe('영화목록 API 테스트', () => {
+  context('인기 영화목록 1페이지를 조회하면', () => {
+    it('20개의 목록을 반환한다.', () => {
+      const baseUrl = 'https://api.themoviedb.org/3/movie/popular';
+      const param = new URLSearchParams({
+        api_key: Cypress.env('API_KEY'),
+        language: 'ko-KR',
+        page: 1,
+      });
+
+      cy.request('GET', `${baseUrl}?${param}`).as('popularMovies');
+
+      cy.get('@popularMovies').its('status').should('eq', 200);
+      cy.get('@popularMovies').its('body.results').should('have.length', 20);
+    });
+  });
+});
+
+describe('영화목록 e2e 테스트', () => {
+  beforeEach(() => {
+    const baseUrl = 'https://api.themoviedb.org/3/movie/popular';
+    const param = new URLSearchParams({
+      api_key: Cypress.env('API_KEY'),
+      language: 'ko-KR',
+      page: 1,
+    });
+
+    cy.intercept(
+      {
+        method: 'GET',
+        url: `${baseUrl}?${param}`,
+      },
+      { fixture: 'movie-popular.json' }
+    ).as('popularMovies');
+
+    cy.visit('http://localhost:8080');
+  });
+
+  context('영화 목록 조회 시', () => {
+    it('800ms 동안 Skeleton UI가 노출되었다가 사라진다.', () => {
+      cy.wait('@popularMovies');
+      cy.get('.skeleton').should('have.length', 60);
+      cy.wait(800);
+      cy.get('.skeleton').should('have.length', 0);
+    });
+  });
+
+  context('페이지 진입 시 인기영화 목록 1페이지를 조회하면', () => {
+    it('20개의 .item-card가 노출된다.', () => {
+      cy.get('.item-card').should('have.length', 20);
+    });
+  });
+
+  context('더보기 버튼을 클릭하면', () => {
+    it('40개의 item-card 노출된다.', () => {
+      cy.get('.btn').click();
+      cy.get('.item-card').should('have.length', 40);
+    });
+  });
+});
+
+describe('짧은 영화목록 e2e 테스트', () => {
+  beforeEach(() => {
+    const baseUrl = 'https://api.themoviedb.org/3/movie/popular';
+    const param = new URLSearchParams({
+      api_key: Cypress.env('API_KEY'),
+      language: 'ko-KR',
+      page: 1,
+    });
+
+    cy.intercept(
+      {
+        method: 'GET',
+        url: `${baseUrl}?${param}`,
+      },
+      { fixture: 'movie-popular-short.json' }
+    ).as('popularMovies');
+
+    cy.visit('http://localhost:8080');
+  });
+
+  context('total_pages 가 1인 인기영화 목록 1페이지를 조회하면', () => {
+    it('18개의 .item-card가 노출된다.', () => {
+      cy.get('.item-card').should('have.length', 18);
+    });
+
+    it('더 보기 버튼이 노출되지 않는다.', () => {
+      cy.contains('더보기').should('not.exist');
+    });
+  });
+});

리뷰: 인기 영화목록 데이터가 18개밖에 없을 때 20개를 그리지 않고 개수를 올바르게 표시하는지 테스트하기 위해 추가해보았습니다!
@@ -0,0 +1,18 @@
+const DEFAULT_SEARCH_PARAMS = {
+  api_key: process.env.API_KEY,
+  language: 'ko-KR',
+};
+const MOVIE_BASE_URL = 'https://api.themoviedb.org/3/movie';
+
+export async function getPopularMovie(page) {
+  const param = new URLSearchParams({
+    ...DEFAULT_SEARCH_PARAMS,
+    page,
+  });
+
+  const response = await fetch(`${MOVIE_BASE_URL}/popular?${param}`);
+  if (response.ok) {
+    return response.json();
+  }
+  return { results: [] };
+}

리뷰: 여러개의 함수를 묶어서 내보내기 위해서 위와같은 종종 사용해왔었는데요!
여기서는 하나의 함수만 내보내기에 제거해보았습니다..!

혹시 여러개의 함수를 export 해야할 때 어떤 방식을 사용하시고 선호하시는지 여쭈어봐도 될까요?
@@ -0,0 +1,12 @@
+import PageHandler from './PageHandler';
+import { getPopularMovie } from '../api/movie';
+
+export async function getNextPopularMovieList() {
+  const { page, done } = PageHandler.next();
+  const { results } = await getPopularMovie(page);
+  return {
+    page,
+    done,
+    nextMovieList: results,
+  };
+}

리뷰: figma를 보고 modal 사용부분이 있어 미리 작성해둔 것인데 말씀해주신 것처럼 유지보수에 어려움을 줄 수 있겠네요..!
명심하도록 하겠습니다! 해당 부분은 제거하였습니다. 감사합니다! 🙇