[TIL] 개인 프로젝트 시작!

곽재훈·2024년 4월 24일
5
post-thumbnail

여는 글

드디어 강의를 한 바퀴 돌고 개인 프로젝트를 집어들었다. 솔직히 5주차 class 문법은 그래도 괜찮았는데, 4주차 this binding 부분이 너무 어려웠다. 나중에 한 번 더 들어야 할듯... 중간에 탈주하고 개인 프로젝트나 하러 가고싶었지만 그래도 공부해야하니 꾹 참고 들었다!. 근데 넘 어려워.

오늘 특강 들으면서 메모를 했는데, 소통에 대한 이야기를 많이 강조하시더라. 사실 캠프를 하면서 당연히 공부도 되겠지만 정말로 같이 하는 사람들이 제일 소중한 선물이 아닐까 하는 생각도 든다. 개발이란 게 실력차이도 나지만 분야도 워낙 다양하다보니 비슷한 환경이나 조건을 가진 사람을 만나는 게 정말로 쉽지 않을텐데 이렇게 많은 사람을 만날 수 있으니 말이다.

주시조 조원들도 10조 조원들도 다들 소중해!


오늘 한 일

  1. 아침에 알고리즘 특강 듣기
  2. 점심까지 내배캠 JS 강의 듣기
  3. 낮에 학습법 특강 듣기
  4. 튜터님이랑 상담하기
  5. 개인 프로젝트 드디어 시작!

개인 프로젝트 시작!

1. API 문서 뒤적거려보기.

드디어 강의를 한 번 다 듣고 개인 프로젝트에 발을 담갔다.
보통은 프로그램을 만든다고 하면, 내가 필요한 기능들을 모두 정리한 후에 그에 맞춰서 프로젝트에 어떤 API가 필요하거나 적절한 지 정리하는 게 순서겠지만, 이번 프로젝트의 경우 TMDB API를 활용해야 한다는 조건이 있었기 때문에 공식 문서를 먼저 뒤져보기로 했다. 내가 뭘 하고 싶은지보다는 API를 이용해서 내가 뭘 할 수 있는지가 더 중요했다.

Fetch를 통해 API를 요청하는 방법은 두 가지 정도인 것 같던데, 둘 다 시도해봤는데 큰 차이는 모르겠었음. 아직 초짜라 그런 것 같기도 하다.

영화를 검색할 때 세 가지 방법이 있다는데, 이번 과제의 주요 목표는 검색 기능이 아니라 불러온 데이터들을 가지고 필터 기능을 만드는 것에 가까워서, 검색을 통해 서버에서 요청하는 기능은 메인 기능 다 만들고 시간 남으면 만들어보기로 하자.

response 객체를 살펴보다가 저기에 "poster_path""overview" 처럼 필수적인 정보들이 있고, "vote_average""release_date"가 있길래 저걸로 별점이나 출시일 필터 기능을 추가로 만들어야 겠다고 생각했다.


2. 기능 정리

볼 거 다 봤으면 이제 필요한 기능을 정리해야 했다.

1) 프로젝트 필수 요구사항

□ Jquery같은 라이브러리를 사용하지 않고 바닐라 자바스크립트만으로 작성하기.
□ TMDB API로 인기 영상 데이터를 가져오기.
□ 영화 정보 리스트 카드 UI로 구현하기. 카드에는 title, overview, poster_path, vote_average가 포함되어 있어야 함.
□ 카드 클릭 시 클릭한 영화 ID가 나와야 함.
□ 영화 검색 UI와 기능 구현.
□ arrow function 사용하기
□ 배열 메서드 사용하기
□ DOM 제어하기

2) 권장 추가사항

□ flex/grid 둘 중 하나 사용하기
□ 웹사이트 랜딩, 새로고침 시 커서가 검색창에 위치하기.
□ 대소문자 구별 X
□ 엔터키로 검색 가능하게 하기.

3) 내가 만들고 싶은 추가사항

□ 한글 입력시 유효성 검사. ㄱ, ㅏ 처럼 자음이나 모음 중에서 하나만 입력되었을 경우, 경고창 표시.
□ 별점과 출시일 데이터에 따른 정렬 기능 추가.
□ 별점 점수에 따라 별점 표시.
□ object hover시 영화 정보가 뜨도록 구현.
□ grid로 예쁜 UI 구현하기


3. 프로젝트 와이어프레임 짜기

정리한 기능을 토대로 와이어프레임을 짜기로 했다.
레퍼런스로 삼은 대상은 역시 넷플릭스! 넷플릭스가 깔끔하고 좋당.



노트북의 제일 일반적인 화면이 1440px이라고 가정하고 1320px12컬럼으로 디자인을 짰다. 1컬럼의 사이즈는 양쪽에 padding: 20px씩을 포함한 110px!. 디자인이라고 하기도 뭐하지만 그래도 와이어프레임이니까 머!
Grid를 이용해서 왼쪽, 오른쪽 번갈아가며 하나씩 대따 큰 카드를 넣어줄 생각이다.
와이어프레임에서 보이는 주요 기능은 필터버튼 누르면 누른 버튼에 따라서 영화 카드 정렬해주기. 검색어 입력하고 검색 누르면 유효성 검사 해주기! 정도인 것 같다.

이후에는 자바스크립트랑 html을 계속 왔다갔다해서 일정한 순서가 있지는 않았다.

이건 중간 완성본! js작업때문에 윗부분은 레이아웃만 짜두고 아랫부분을 먼저 완성해서 위에는 아직 손을 못 댔다.


4. 코드 구성

오늘 공부법 특강을 들을 때, 짧은 코드를 작성하더라도 왜 그 코드를 작성하고 어떤 의도나 고민을 가지고 나온 코드인지 생각하는 게 중요하다고 하셔서 코드에 대해서 적어보려고 한다.

프로젝트의 메인 기능은 1. 영화 데이터를 받아오고, 2. 데이터를 카드로 만들어 페이지에 보여주기.


const api_key = "Bearer 비밀번호비밀번호~~;

const options = {
  method: "GET",
  headers: {
    accept: "application/json",
    Authorization: api_key,
  },
};

맨 위에는 api관련 api_key와 인증에 필요한 options 객체를 정의해두었다.

class Movie {
  constructor(id, title, originalTitle, overview, posterPath, voteAverage) {
    this.id = id;
    this.title = title;
    this.originalTitle = originalTitle;
    this.overview = overview;
    this.posterPath = posterPath;
    this.voteAverage = voteAverage;
  }
}

이번에 5주차 강의에서 새로 배운 class문법을 써보기로 했다.
API를 통해 데이터를 받아오면,

이와 같은 형태로, 나에게 필요하지 않은 속성들도 많기 때문에 필요한 정보만 골라담고 싶어서 Movie라는 class를 만들었다.

const getPopularMovies = async () => {
    let path = "/movie/popular";
    let data = await getData(path);
    data = getMoviesFromJSON(data);
    console.log("Success!");
    return data;
}

인기영화를 가져오는 함수인 getPopularMovies()/movie/popular를 path로 입력해줘야해서 함수 내부에서 선언해주고 data 변수에 서버로부터 데이터를 받아오는 getData() 함수를 실행한다. getData()에 인자로 path가 전달되었는데, getData() 함수를 같이 보자.


const domain = "https://api.themoviedb.org/3";
const imageEndPoint = "https://image.tmdb.org/t/p/w500";

const getData = async (query) => {
  let response = await fetch(domain + query, options);
  try {
    response = response.json();
    return response;
  } catch {
    console.log("response is empty!");
  }
};

getData() 함수는 query라는 매개변수를 가지는데 let responsefetch함수의 값을 받는다. domain 뒤에 query에는 제가 좀 전에 전달했던 path가 들어가있쥬? 그러면 결론적으로 await fetch("https://api.themoviedb.org/3/movie/popular")가 실행된다고 볼 수 있다! fetch 요청은 바로 실행되지 않고 시간이 걸릴 수 있는 비동기 함수이기때문에 async/await 문법을 사용해서 fetch 작업이 완료될 때 까지 다음 코드가 기다릴 수 있도록 했다.
그 이후에 response.json()을 통해서 제가 사용할 수 있도록 객체 형태로 만들어주고 다시 response에 저장한 뒤 return 해준다.

const getPopularMovies = async () => {
    let path = "/movie/popular";
    let data = await getData(path);
    data = getMoviesFromJSON(data);
    console.log("Success!");
    return data;
}

그러면 다시 여기로 돌아오는데, 현재 getData()의 리턴값이 data에 저장되어 있다. 그러면 data를 다시 getMoviesFromJSON()함수의 인자로 넘겨주는데,

const getMoviesFromJSON = (json) => {
  return json.results.map(movie => {
    const currMovie = new Movie(
      movie.id,
      movie.title,
      movie.original_title,
      movie.overview,
      movie.poster_path,
      movie.vote_average);
    return currMovie;
  })
}

getMoviesFromJSON()함수는 json.results에 들어있는 영화 데이터 객체들을 map method로 순회하면서 필요한 정보들을 추출하고 new Movie로 새로운 인스턴스를 생성하여 currMovie에 할당하고 반환한다.

const getPopularMovies = async () => {
    let path = "/movie/popular";
    let data = await getData(path);
    data = getMoviesFromJSON(data);
    console.log("Success!");
    return data;
}

그러면 data에 다시 방금 실행되었던 getMoviesFromJSON()의 결과값으로 영화 정보들로 만들어진 인스턴스들이 담긴 배열이 반환되고 그대로 리턴한다.

현재 data 안에는,

Movie라는 class의 인스턴스로 만들어진 20개의 요소가 들어있다.

const domain = "https://api.themoviedb.org/3";
const imageEndPoint = "https://image.tmdb.org/t/p/w500";

class Movie {
  constructor(id, title, originalTitle, overview, posterPath, voteAverage) {
    this.id = id;
    this.title = title;
    this.originalTitle = originalTitle;
    this.overview = overview;
    this.posterPath = posterPath;
    this.voteAverage = voteAverage;
  }
}

const getMoviesFromJSON = (json) => {
  return json.results.map(movie => {
    const currMovie =  new Movie(
      movie.id,
      movie.title,
      movie.original_title,
      movie.overview,
      movie.poster_path,
      movie.vote_average);
    return currMovie;
  })
}

const getData = async (query) => {
  let response = await fetch(domain + query, options);
  try {
    response = response.json();
    return response;
  } catch {
    console.log("response is empty!");
  }
};

const getPopularMovies = async () => {
    let path = "/movie/popular";
    let data = await getData(path);
    data = getMoviesFromJSON(data);
    console.log("Success!");
    return data;
}

그래서 정리하면 현재 이런 상태!

const makeMovieArticle = async () => {
  /* 방금까지 실행된 함수의 결과가 movies에 저장됨. */
  let movies = await getPopularMovies();
  /* 만약 검색 버튼이 눌려서 함수가 실행되었다면, text와 movie의 title을 비교하여 filtering을 해줍니다. */
  if(filteringText) {
    movies = movies.filter(movie => {
      const movieTitle = movie.title.toUpperCase();
      return movieTitle.includes(filteringText.toUpperCase());
    })
  }
  /* 만약 body에 이미 ul이 있다면 제거합니다 */
  const isContent = document.querySelector(".content-box ul");
  if(isContent) {
    movieArticle.removeChild(isContent);
  }
  
  const ul = document.createElement('ul');
  /* 영화 정보가 담긴 카드를 담을 container를 생성! */
  let moviesHTML = movies.map(movie => makeMovieCard(movie));
  
  moviesHTML.forEach((movie, i) => {
    
      const ratingWidth = parseInt(movie.childNodes[1].childNodes[2].style.width);
      console.log(ratingWidth);
      movie.childNodes[1].childNodes[2].style.width = `${ratingWidth * 2}px`;
    } 
    ul.appendChild(movie);
  });
  console.log(ul);
  movieArticle.appendChild(ul);
}

/* text를 p태그로 감싸 반환. */ 
const makeParagraphNode = (text) => {
  const pTag = document.createElement('p');
  const content = document.createTextNode(text);
  pTag.appendChild(content);
  return pTag;
}

/* 영화 객체를 인자로 받아서 card에 쓰일 Node를 만들어 리턴 */
function makeMovieCard(movie) {
  const li = document.createElement('li');
  li.classList.add("movie-card");
  li.dataset._id = movie.id;
  const title = movie.title;
  const originalTitle = movie.originalTitle;
  const posterPath = movie.posterPath;
  const voteAverage = movie.voteAverage;
  const overview = movie.overview;
  // console.log(title, originalTitle, posterPath);

  const posterNode = document.createElement('div');
  posterNode.classList.add("movie-poster");
  posterNode.style.backgroundImage = `url(${imageEndPoint+posterPath})`;
  
  const titleNode = makeParagraphNode(title);
  titleNode.classList.add('movie-title');

  const infoNode = document.createElement('div');
  infoNode.classList.add('movie-info');

  li.appendChild(posterNode);
  
  infoNode.appendChild(titleNode);

  //만약 영화 제목이 오리지널 제목과 다르다면, 오리지널 제목 추가.(한국, 프랑스 등)
  if(movie.title !== movie.originalTitle) {
    const originalTitleNode = makeParagraphNode(originalTitle);
    originalTitleNode.classList.add("movie-original-title");
    infoNode.appendChild(originalTitleNode);
  }

  const overviewNode = makeParagraphNode(overview);
  overviewNode.classList.add("overview");

  const ratingScoreNode = document.createElement('div');
  ratingScoreNode.classList.add('rating-score');
  ratingScoreNode.style.width = `${Math.floor(voteAverage)*25}px`;
  console.log(voteAverage);
  infoNode.appendChild(overviewNode);
  infoNode.appendChild(ratingScoreNode);
  

  li.appendChild(infoNode);
  return li;
}

/* 카드를 누르면 ID가 뜨도록 이벤트리스너 등록. */
const addBtnEvent = () => {
  const movieCards = document.querySelectorAll('.movie-card');
  const hotMovieCards = document.querySelectorAll('.hot-movie-card');
  movieCards.forEach(card => {
    card.addEventListener('click', ()=> {
      alert(card.dataset._id);
    });
  });

  hotMovieCards.forEach(card => {
    card.addEventListener('click', ()=> {
      alert(card.dataset._id);
    });
  });
  console.log(movieCards);
}

5. 정리

1) 프로젝트 필수 요구사항

☑︎ Jquery같은 라이브러리를 사용하지 않고 바닐라 자바스크립트만으로 작성하기.
☑︎ TMDB API로 인기 영상 데이터를 가져오기.
☑︎ 영화 정보 리스트 카드 UI로 구현하기. 카드에는 title, overview, poster_path, vote_average가 포함되어 있어야 함.
☑︎ 카드 클릭 시 클릭한 영화 ID가 나와야 함.
☑︎ 영화 검색 UI와 기능 구현.
☑︎ arrow function 사용하기
☑︎ 배열 메서드 사용하기
☑︎ DOM 제어하기

2) 권장 추가사항

☑︎ flex/grid 둘 중 하나 사용하기
□ 웹사이트 랜딩, 새로고침 시 커서가 검색창에 위치하기.
☑︎ 대소문자 구별 X
□ 엔터키로 검색 가능하게 하기.

3) 내가 만들고 싶은 추가사항

☑︎ 한글 입력시 유효성 검사. ㄱ, ㅏ 처럼 자음이나 모음 중에서 하나만 입력되었을 경우, 경고창 표시.
□ 별점과 출시일 데이터에 따른 정렬 기능 추가.
☑︎ 별점 점수에 따라 별점 표시.
☑︎ object hover시 영화 정보가 뜨도록 구현.
☑︎ grid로 예쁜 UI 구현하기

목표를 다 채우지는 못했지만, 주요 로직은 다 구현했으니 내일 마무리할 수 있을 것 같다.


카드 호버시 별점이 시각적으로 표시됨.

background-image와 background-size를 통해 이미지를 고정시키고 width를 자바스크립트로 할당했다. bacground-image의 사이즈는 고정인데 width의 값을 변경해서 overflow: hidden처럼, 부모 요소를 벗어나는 크기는 보이지 않는 점을 이용했다.별점이 1에서 10까지 있기 때문에 Math.floor를 통해서 숫자를 정수로 내림하고 거기다가 별이 담긴 오브젝트의 길이를 10으로 나누고 별점만큼 곱했다.

큰 박스는 더 크게 나오도록 CSS를 설정했다.

ㄱ이나 ㅏ처럼 초성이나 중성만 입력되면 검색이 불가능하다.

검색 기능도 잘 작동하는 모양이다!

아직 못 한건 내일 하는걸로!

profile
개발하고 싶은 국문과 머시기

2개의 댓글

comment-user-thumbnail
2024년 4월 25일

역시 대단하십니다... 정말 잘하시네요 전 아직 잘 못하겠는데 ㅠㅠ

1개의 답글

관련 채용 정보