하루 하나씩 작성하는 TIL #10
#10에선 이번 주차에 진행한 개인 프로젝트에 대해 회고해보려고 한다.
다음 주 코드 부검 예정이 있기 때문에 꼼꼼하게 리뷰하여 혼나지 않겠다...
(개인적인 바램)
2주차 개인 과제는 영화 검색 사이트를 제작하는 것이었다.
바닐라 js만을 사용 (제이쿼리 불가)
TMDB 사용 (#6에서 설명)
TMDB에서 받아온 데이터를 브라우저 화면에 카드 형태로 보여주기
영화 검색 UI 구현
const, let 만을 사용하여 변수 선언
화살표 함수 사용
배열 메소드 사용 (forEach, map, filter, reduce, find)중 2개 이상 사용
DOM 제어 ( 아래 API 목록 중 2개 이상 사용)
💡 1. 문서 객체 생성과 선택
document.createElement(tagName)
: 새로운 HTML 요소를 생성합니다.document.getElementById(id)
: id 속성을 기준으로 요소를 선택합니다.document.getElementsByTagName(name)
: 태그 이름을 기준으로 요소를 선택합니다.document.getElementsByClassName(name)
: 클래스 이름을 기준으로 요소를 선택합니다.document.querySelector(selector)
: CSS 선택자를 이용하여 요소를 선택합니다.document.querySelectorAll(selector)
: CSS 선택자를 이용하여 모든 요소를 선택합니다.element.innerHTML
: 해당 요소 내부의 HTML 코드를 변경합니다.element.textContent
: 해당 요소 내부의 텍스트를 변경합니다.element.setAttribute(attr, value)
: 해당 요소의 속성 값을 변경합니다.element.getAttribute(attr)
: 해당 요소의 속성 값을 가져옵니다.element.style.property
: 해당 요소의 스타일 값을 변경합니다.element.appendChild(child)
: 해당 요소의 하위 요소로 child를 추가합니다.element.removeChild(child)
: 해당 요소의 하위 요소 중 child를 삭제합니다.element.classList.add(class)
: 해당 요소의 클래스에 새로운 클래스를 추가합니다.element.classList.remove(class)
: 해당 요소의 클래스 중에서 특정 클래스를 제거합니다.element.classList.toggle(class)
: 해당 요소의 클래스 중에서 특정 클래스를 추가 또는 제거합니다.element.addEventListener(type, listener)
: 해당 요소에서 이벤트가 발생했을 때 호출할 함수를 등록합니다.element.removeEventListener(type, listener)
: 해당 요소에서 등록된 함수를 제거합니다.event.preventDefault()
: 이벤트가 발생했을 때 기본 동작을 취소합니다.event.stopPropagation()
: 이벤트의 버블링을 방지하기 위해 이벤트 전파를 중지합니다.window.location.href
: 현재 페이지의 URL을 가져옵니다.window.alert(message)
: 경고 메시지를 출력합니다.window.confirm(message)
: 확인 메시지를 출력하고 사용자의 답변에 따라 Boolean 값을 반환합니다.나름 나쁘지는 않다고 생각하면서 부검했는데요, 튜터님의 피드백에 따라 2차 부검이 있을 수 있습니다 (있을 예정)
필자가 작성한 과정에 따라 정리해보았다.
document.addEventListener('DOMContentLoaded', function () {
const options = {
method: 'GET',
headers: {
accept: 'application/json',
Authorization: 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJmMmZiNGNjMDVlY2UzYmVlYzA5YzFlZWE0MTA1YjY1ZSIsInN1YiI6IjY2MjY0MWM5NjNlNmZiMDE3ZWZjZDU5ZCIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.iV4EB4KeKrhxS9-JgkN2hfI9gkbQb5GmenzTWvEdv8A'
}
};
// 영화 정보 불러오기
fetch('https://api.themoviedb.org/3/movie/top_rated?language=en-US&page=1', options)
.then(response => response.json())
.then(data => displayMovies(data.results))
.catch(err => console.error(err));
// 14번줄까지 tdmb 세팅 코드.
TMDB에서 복붙해온 API 코드.
document.addEventListener('DOMContentLoaded', function () {
위 코드는 이벤트를 대기하는 메서드 중 하나로, html 문서의 특정 이벤트를 감지하는데 사용 된다.
DOMContentLoaded는 html 문서의 로드가 완료되었을 때 발생되는 이벤트를 나타낸다.
위 코드를 위에 꼭 넣어줘야 하는 이유가 무엇인가 궁금했었다. 그냥 쓰면 되지 않나 생각했었는데 넣어줘야하는 이유가 있다.
DOMContentLoaded 이벤트 핸들러 내에 작성된 JavaScript 코드는 HTML 문서의 로드가 완료된 후에 실행된다. 이 코드를 넣는 이유는 JavaScript 코드가 HTML 문서에 접근하여 DOM 요소를 조작하거나 이벤트를 처리하기 위해서인데, html 문서가 아직 완전히 로드되기 전 js 코드가 실행되면 dom 요소들이 아직 생성이 안됐을 수 있기 때문에 에러 발생 가능성이 있다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>A03조 김도연 영화 검색 사이트</title>
<link rel="stylesheet" href="styles.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Black+Han+Sans&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/typeit/dist/typeit.min.js"></script>
</head>
<body>
<header>
<p id="multipleStrings"></p>
</header>
<div class="search-container">
<input type="text" id="searchInput" placeholder="영화 제목을 입력해주세요" autofocus>
<button id="searchButton">Search</button>
</div>
<div id="movieContainer" class="movie-container"></div>
<footer>
<div class="footer-content">
<a href="https://github.com/eldoradodo">
<img class="footer-img" src="img\git.png" alt="GitHub">
</a>
<a href="https://velog.io/@eldoradodo/posts">
<img class="footer-img" src="img\velog.jpg" alt="Velog">
</a>
</div>
</footer>
<script src="movie.js"></script>
</body>
</html>
필자가 작성한 html 코드.
요구하는 css 사항을 만족하였기 때문에 추가적으로 글꼴, typeit을 사용하였다.
body에는 완성본 예시와 비슷하게 헤더, text input, button, 영화 이미지와 정보가 들어갈 movieContainer, 추가적으로 footer를 제작하여 벨로그 링크와 github 링크를 이미지로 삽입하였다.
header {
text-align: center;
background-color: #9a95ff;
padding: 20px 0;
/*위아래 여백*/
}
/*헤더 글꼴*/
header p#multipleStrings {
font-family: "Black Han Sans", sans-serif;
font-weight: 300;
font-size: 35px;
color: white;
font-style: normal;
}
/* Flex 사용 */
.search-container {
display: flex;
justify-content: center;
/*수평 가운데 정렬*/
margin-bottom: 20px;
}
/* Grid 사용 */
.movie-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
/*그리드 컨테이너의 열 크기 지정. 그리드 셀의 수 자동으로 조정, (셀의 최소크기, 최대 크기(가능한 늘리기))*/
gap: 20px;
/*그리드 셀 사이 간격*/
}
/* 카드 스타일 */
.movie-card {
border: 1px solid #7169ff;
border-radius: 8px;
border-width: 5px;
padding: 10px;
background-color: #9a95ff;
}
.movie-card img {
width: 100%;
border-radius: 8px;
}
.movie-card h3 {
margin: 10px 0;
}
/* 알림 스타일 */
.alert {
background-color: #9a95ff;
color: #333;
padding: 15px;
border-radius: 8px;
margin: 20px auto;
width: fit-content;
/*알림의 크기 너비에 맞게 자동 조절*/
}
.search-container {
margin-top: 30px;
margin-bottom: 30px;
}
.movie-container {
margin-bottom: 30px;
/* 카드들 아래에 간격 추가 */
}
/* 서치 버튼 스타일 */
#searchButton {
background-color: #9a95ff;
color: white;
border-color: black;
padding: 10px 20px;
/* 내부 여백 */
text-align: center;
text-decoration: none;
font-size: 16px;
border-radius: 20px;
margin-left: 10px;
/* 텍스트 박스와의 간격 */
}
/*텍스트박스 스타일*/
#searchInput {
width: 300px;
/* 텍스트 박스 너비 조절 */
border-radius: 10px;
}
/* 서치 컨테이너 스타일 */
.search-container {
display: flex;
justify-content: center;
margin-bottom: 30px;
/* 텍스트 박스와 서치 버튼 사이의 간격 */
}
footer {
background-color: #9a95ff;
/* 연노랑색 배경 */
padding: 30px;
text-align: right;
}
.footer-content {
display: flex;
justify-content: flex-end;
}
.footer-content a {
margin-left: 10px;
}
.footer-img {
width: 40px;
border-radius: 50%;
/* 모서리를 둥글게 함 */
}
/*선택 요구 사항*/
/*flex, grid 사용 O*/
/*순수 css 사용 O*/
선택 요구사항 中 순수 css, flex, gird 사용을 만족시킨 코드
document.addEventListener('DOMContentLoaded', function () {
const options = {
method: 'GET',
headers: {
accept: 'application/json',
Authorization: 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJmMmZiNGNjMDVlY2UzYmVlYzA5YzFlZWE0MTA1YjY1ZSIsInN1YiI6IjY2MjY0MWM5NjNlNmZiMDE3ZWZjZDU5ZCIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.iV4EB4KeKrhxS9-JgkN2hfI9gkbQb5GmenzTWvEdv8A'
}
};
// 영화 정보 불러오기
fetch('https://api.themoviedb.org/3/movie/top_rated?language=en-US&page=1', options)
.then(response => response.json())
.then(data => displayMovies(data.results))
.catch(err => console.error(err));
첫 번쨰 과정에서 tmdb에서 복붙해온 api코드
const options = { ... };는 http 요청 옵션을 정의하는 객체로 fetch함수로 전달되어 API에서 영화 정보를 가져오는 데 사용된다.
fetch()함수는 브라우저 내장 API중 하나로 , 네트워크 요청을 생성하고 해당 요청에 대한 응답을 Promise 형태로 반환한다. 이 함수를 사용 시 서버로부터 데이터를 가져오거나 전송할 수 있다.
.then()은 Promise 객체가 성공적으로 처리될 때 실행되는 메소드
.catch()는 오류를 처리하는 메소드
const searchInput = document.getElementById('searchInput');
const searchButton = document.getElementById('searchButton');
const movieContainer = document.getElementById('movieContainer');
id가 각각 괄호 안에 들어있는 요소를 찾아서 각각 변수에 할당. 이 변수를 통해 js에서 조작 가능
searchInput.focus();
선택 요구사항 중 커서 자동 위치를 만족시키는 함수
function displayMovies(movies) {
movieContainer.innerHTML = '';
이 함수 내의 매개변수 movies는
.then(data => displayMovies(data.results))
여기서 받아온 데이터이다. 즉 movies 변수는 tmdb api로부터 가져온 영화 데이터를 담고있다.
movieContainer안에 있는 html 요소의 내용을 비운다. 새로운 영화 목록이 표시될 때 이전의 영화 목록이 그대로 남아있게 될 수 있기 때문에 필수적이진 않지만 사용해주는 것이 좋다.
movies.forEach(movie => { //요구사항 - forEach 사용
const {
id,
title,
overview,
poster_path,
vote_average
} = movie;
const imageUrl = poster_path ? `https://image.tmdb.org/t/p/w500${poster_path}` : 'https://via.placeholder.com/150';
배열의 각 요소에 대해 한 번씩 콜백 함수를 실행한다. movies 배열의 각 요소에 대해 반복하며 작업을 수행한다.
movie의 객체 속성들을 추출해준다. 고유 식별자, 제목, 개요, 포스터 이미지 경로, 평균 평점을 추룰한다.
포스터 이미지 경로는 3항 연산자를 사용하여 이미지가 있는 경우엔 tmdb에서 가져오고 없는 경우ㄴ엔 대체 이미지 경로를 사용한다.
movie 변수는 각각 영화 정보를 담고있는 객체다. 메소드가 반복되는 동안 현재 처리 중인 영화 정보를 나타낸다.
= movie;
이 부분은 객체 디스트럭처링을 사용하는 부분이다. 이렇게 하면 movie 객체에 속성들을 각각의 변수에 할당할 수 있다.
저 방법을 쓰지 않고 일반적인 방법을 사용한다면,
movies.forEach(movie => {
// 속성에 직접 접근하여 정보를 사용합니다.
const id = movie.id;
const title = movie.title;
const overview = movie.overview;
const poster_path = movie.poster_path;
const vote_average = movie.vote_average;
}
위와 같이 표현할 수 있다.
const movieCard = document.createElement('div');
새로운 div 요소를 생성하여 할당한다. 영화 정보를 담을 컨테이너.
movieCard.classList.add('movie-card');
생성한 div 요소에 movie-card 클래스를 추가. css 스타일링을 위해 사용된다.
movieCard.innerHTML = `
<img src="${imageUrl}" alt="${title}">
<h3>${title}</h3>
<p>${overview}</p>
<p>평점: ${vote_average}</p>
`;
moviecard 요소의 내부 html을 설정한다. 템플릿 문자열을 사용하여 영화 정보를 포함한 html을 동적 생성해준다.
imageUrl 변수에서 가져온 이미지를 할당하고 title 속성을 이미지의 alt 속성에 할당하여 대체 텍스트를 제공한다.
movieCard.addEventListener('click', () => {
movies.map(movie => { //요구사항 - map 사용
if (movie.id === id) {
alert(`선택한 영화 ID: ${movie.id}`);
}
});
});
movieContainer.appendChild(movieCard);
});
}
클릭 이벤트 리스너를 추가. 영화 카드 클릭시 실행
map() 메서드를 통해 모든 영화를 순회하고, 각 영화의 id 속성을 확인하여 사용자가 클릭한 영화를 찾는다. 일치하는 경우, id를 알려주는 alert 창을 띄워준다.
movieContainer.appendChild(movieCard);를 통해 생성된 movieCard 요소를 movieContainer에 추가하여 화면에 표시한다.
searchInput.addEventListener('keypress', function (e) {
if (e.key === 'Enter') {
searchButton.click();
}
});
특정 키가 눌렸을 때 실행되는 이벤트 리스너
이벤트 객체의 키 속성이 enter인지 확인 후 맞다면 아래 코드가 실행.
new TypeIt("#multipleStrings", {
strings: ["내일 배움 캠프", "영화 검색 사이트"],
speed: 50,
waitUntilVisible: true,
}).go();
});
이상 개인 프로젝트 회고 끝!
깃허브