드디어 강의를 한 바퀴 돌고 개인 프로젝트를 집어들었다. 솔직히 5주차 class 문법은 그래도 괜찮았는데, 4주차 this binding 부분이 너무 어려웠다. 나중에 한 번 더 들어야 할듯... 중간에 탈주하고 개인 프로젝트나 하러 가고싶었지만 그래도 공부해야하니 꾹 참고 들었다!. 근데 넘 어려워.
오늘 특강 들으면서 메모를 했는데, 소통에 대한 이야기를 많이 강조하시더라. 사실 캠프를 하면서 당연히 공부도 되겠지만 정말로 같이 하는 사람들이 제일 소중한 선물이 아닐까 하는 생각도 든다. 개발이란 게 실력차이도 나지만 분야도 워낙 다양하다보니 비슷한 환경이나 조건을 가진 사람을 만나는 게 정말로 쉽지 않을텐데 이렇게 많은 사람을 만날 수 있으니 말이다.
주시조 조원들도 10조 조원들도 다들 소중해!
드디어 강의를 한 번 다 듣고 개인 프로젝트에 발을 담갔다.
보통은 프로그램을 만든다고 하면, 내가 필요한 기능들을 모두 정리한 후에 그에 맞춰서 프로젝트에 어떤 API가 필요하거나 적절한 지 정리하는 게 순서겠지만, 이번 프로젝트의 경우 TMDB API를 활용해야 한다는 조건이 있었기 때문에 공식 문서를 먼저 뒤져보기로 했다. 내가 뭘 하고 싶은지보다는 API를 이용해서 내가 뭘 할 수 있는지가 더 중요했다.
Fetch를 통해 API를 요청하는 방법은 두 가지 정도인 것 같던데, 둘 다 시도해봤는데 큰 차이는 모르겠었음. 아직 초짜라 그런 것 같기도 하다.
영화를 검색할 때 세 가지 방법이 있다는데, 이번 과제의 주요 목표는 검색 기능이 아니라 불러온 데이터들을 가지고 필터 기능을 만드는 것에 가까워서, 검색을 통해 서버에서 요청하는 기능은 메인 기능 다 만들고 시간 남으면 만들어보기로 하자.
response
객체를 살펴보다가 저기에 "poster_path"
나 "overview"
처럼 필수적인 정보들이 있고, "vote_average"
나 "release_date"
가 있길래 저걸로 별점이나 출시일 필터 기능을 추가로 만들어야 겠다고 생각했다.
볼 거 다 봤으면 이제 필요한 기능을 정리해야 했다.
□ Jquery같은 라이브러리를 사용하지 않고 바닐라 자바스크립트만으로 작성하기.
□ TMDB API로 인기 영상 데이터를 가져오기.
□ 영화 정보 리스트 카드 UI로 구현하기. 카드에는 title, overview, poster_path, vote_average가 포함되어 있어야 함.
□ 카드 클릭 시 클릭한 영화 ID가 나와야 함.
□ 영화 검색 UI와 기능 구현.
□ arrow function 사용하기
□ 배열 메서드 사용하기
□ DOM 제어하기
□ flex/grid 둘 중 하나 사용하기
□ 웹사이트 랜딩, 새로고침 시 커서가 검색창에 위치하기.
□ 대소문자 구별 X
□ 엔터키로 검색 가능하게 하기.
□ 한글 입력시 유효성 검사. ㄱ, ㅏ 처럼 자음이나 모음 중에서 하나만 입력되었을 경우, 경고창 표시.
□ 별점과 출시일 데이터에 따른 정렬 기능 추가.
□ 별점 점수에 따라 별점 표시.
□ object hover시 영화 정보가 뜨도록 구현.
□ grid로 예쁜 UI 구현하기
정리한 기능을 토대로 와이어프레임을 짜기로 했다.
레퍼런스로 삼은 대상은 역시 넷플릭스! 넷플릭스가 깔끔하고 좋당.
노트북의 제일 일반적인 화면이 1440px
이라고 가정하고 1320px
에 12컬럼
으로 디자인을 짰다. 1컬럼
의 사이즈는 양쪽에 padding: 20px
씩을 포함한 110px
!. 디자인이라고 하기도 뭐하지만 그래도 와이어프레임이니까 머!
Grid를 이용해서 왼쪽, 오른쪽 번갈아가며 하나씩 대따 큰 카드를 넣어줄 생각이다.
와이어프레임에서 보이는 주요 기능은 필터버튼 누르면 누른 버튼에 따라서 영화 카드 정렬해주기. 검색어 입력하고 검색 누르면 유효성 검사 해주기! 정도인 것 같다.
이후에는 자바스크립트랑 html을 계속 왔다갔다해서 일정한 순서가 있지는 않았다.
이건 중간 완성본!
js작업때문에 윗부분은 레이아웃만 짜두고 아랫부분을 먼저 완성해서 위에는 아직 손을 못 댔다.
오늘 공부법 특강을 들을 때, 짧은 코드를 작성하더라도 왜 그 코드를 작성하고 어떤 의도나 고민을 가지고 나온 코드인지 생각하는 게 중요하다고 하셔서 코드에 대해서 적어보려고 한다.
프로젝트의 메인 기능은 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 response
에 fetch
함수의 값을 받는다. 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);
}
☑︎ Jquery같은 라이브러리를 사용하지 않고 바닐라 자바스크립트만으로 작성하기.
☑︎ TMDB API로 인기 영상 데이터를 가져오기.
☑︎ 영화 정보 리스트 카드 UI로 구현하기. 카드에는 title, overview, poster_path, vote_average가 포함되어 있어야 함.
☑︎ 카드 클릭 시 클릭한 영화 ID가 나와야 함.
☑︎ 영화 검색 UI와 기능 구현.
☑︎ arrow function 사용하기
☑︎ 배열 메서드 사용하기
☑︎ DOM 제어하기
☑︎ flex/grid 둘 중 하나 사용하기
□ 웹사이트 랜딩, 새로고침 시 커서가 검색창에 위치하기.
☑︎ 대소문자 구별 X
□ 엔터키로 검색 가능하게 하기.
☑︎ 한글 입력시 유효성 검사. ㄱ, ㅏ 처럼 자음이나 모음 중에서 하나만 입력되었을 경우, 경고창 표시.
□ 별점과 출시일 데이터에 따른 정렬 기능 추가.
☑︎ 별점 점수에 따라 별점 표시.
☑︎ object hover시 영화 정보가 뜨도록 구현.
☑︎ grid로 예쁜 UI 구현하기
목표를 다 채우지는 못했지만, 주요 로직은 다 구현했으니 내일 마무리할 수 있을 것 같다.
카드 호버시 별점이 시각적으로 표시됨.
background-image와 background-size를 통해 이미지를 고정시키고 width를 자바스크립트로 할당했다. bacground-image의 사이즈는 고정인데 width의 값을 변경해서 overflow: hidden처럼, 부모 요소를 벗어나는 크기는 보이지 않는 점을 이용했다.별점이 1에서 10까지 있기 때문에 Math.floor를 통해서 숫자를 정수로 내림하고 거기다가 별이 담긴 오브젝트의 길이를 10으로 나누고 별점만큼 곱했다.
큰 박스는 더 크게 나오도록 CSS를 설정했다.
ㄱ이나 ㅏ처럼 초성이나 중성만 입력되면 검색이 불가능하다.
검색 기능도 잘 작동하는 모양이다!
아직 못 한건 내일 하는걸로!
역시 대단하십니다... 정말 잘하시네요 전 아직 잘 못하겠는데 ㅠㅠ