본문 바로가기

TIL

TIL - 실시간 검색 기능과 반응형 웹 만들어 보기

개인 과제의 필수 요구 사항을 충족시키고 themoviedb에서 받아온 데이터가 top_rated인 것을 고려해서 추가 사항으로 기능들을 추가하기로 생각했다.

기능을 추가하는 데 있어서 중요하게 생각한 점은 사이트의 목적과 맞고 내가 사용자라면 있었으면 좋을 것이라고 생각한 것들을 적어보고 하나씩 구현했다.

  • 배지를 만들어서 카드의 왼쪽 상단에 순위를 더 명확하게 파악
  • 다크 모드/ 라이트 모드 구현과 localStorage에 저장해서 설정 저장
  • 검색 기능이 사이트 내에 데이터를 조회하는 것이라서 실시간으로 검색 결과 표시
  • 데이터 베이스에서 받아온 데이터를 확인해 보니 출시 연도가 고르게 분포되어 있어서 연도순 필터링해서 화면에 표시
  • 반응형 웹 만들기

 

결과물

 

 

 

1. 배지를 만들어서 카드의 왼쪽 상단에 순위 만들기


// 영화 순위 뱃지 생성 함수
const createPlaceBadge = (index) => {
    const placeBadge = document.createElement('div');
    placeBadge.classList.add('place-badge');
    placeBadge.textContent = `${index + 1}`;
    return placeBadge;
};

// 영화 카드 생성 함수
const createMovieCard = (movie, index) => {
    const movieCard = document.createElement("div");
    movieCard.classList.add("movie-card");
    movieCard.id = `${movie.id}`;
	const img = document.createElement("img");
    img.src = `https://image.tmdb.org/t/p/w500${movie.poster_path}`;
    img.alt = movie.title;

    const title = createMovieTitle(movie.title); // 영화 타이틀 요소 생성
    const content = createMovieContent(movie.overview); // 영화 소개 요소 생성
    const rating = createMovieRating(movie.vote_average); // 영화 평점 요소 생성
    const placeBadge = createPlaceBadge(index); // 영화 뱃지 생성

    movieCard.append(img, title, rating, content, placeBadge);
    return movieCard;
};

배지는 간단하게 템플릿 리터럴 문법을 이용해서 index에 + 1을 추가해서 카드와 배지에 css position을 이용해서 위치시켰다. 배지를 만드는 것은 간단했지만 시각적으로 한눈에 순위를 파악이 가능해서 사용자 경험을 향상한 것 같았다.

 

 

2. 다크 모드/ 라이트 모드 


// 라이트 모드 토글 함수
const toggleLightmode = () => {
    const body = document.body;
    const lightModeEnabled = !body.classList.contains('light-mode');
    body.classList.toggle('light-mode');

    // 라이트 모드 상태를 localStorage에 저장
    localStorage.setItem('lightModeEnabled', lightModeEnabled);
};

// 페이지 로드 시 localStorage에서 라이트 모드 설정 가져오기
window.addEventListener('DOMContentLoaded', () => {
    const lightModeEnabled = localStorage.getItem('lightModeEnabled') === 'true';
    const lightmodeToggle = document.getElementById('light-mode-toggle');

    // 라이트 모드 설정이 저장된 경우에만 라이트 모드를 활성화하고 체크박스를 업데이트합니다.
    if (lightModeEnabled) {
        toggleLightmode();
        lightmodeToggle.checked = true;
    } else {
        lightmodeToggle.checked = false;
    }
});

// 라이트 모드 토글 체크박스의 이벤트 리스너
document.getElementById('light-mode-toggle').addEventListener('change', toggleLightmode);

처음 계획할 때 기본 배경색을 어두운 색으로 디자인해서 라이트 모드라고 이름을 지었고 body에 toggle 기능을 이용해서 클래스 light-mode를 넣어주고 제거해 주는 간단한 기능이었다. 체크가 되지 않아야 light-mode기 때문에 contains앞에 body에! 인 논리 부정 연산자를 넣어서 체크 박스를 업데이트하면서 localStorage에 설정을 저장해서 가져왔다.

 

 

3. 실시간 검색 기능


// 영화 카드들을 보이도록 설정하는 함수
const showMovieCards = () => {
    const movieCards = document.querySelectorAll(".movie-card");
    movieCards.forEach(movieCard => {
        movieCard.style.display = "block";
    });
};
// 입력값에 해당하는 영화 필터링 함수
const filterMovies = (inputValue) => {
    const movieCards = document.querySelectorAll(".movie-card");
    movieCards.forEach(movieCard => {
        const title = movieCard.querySelector(".movie-title").textContent.toLowerCase();
        movieCard.style.display = title.includes(inputValue.toLowerCase()) ? "block" : "none";
    });
};
// 실시간 검색 기능
window.addEventListener('DOMContentLoaded', () => {
    // HTML 요소를 DOM으로 가져오기
    const searchInput = document.getElementById("search-input");
    const headerTitle = document.getElementById("header-title");
    const inputClearBtn = document.getElementById("input-clear-btn");
    const movieListHeading = document.getElementById("movie-list-heading");

    headerTitle.addEventListener("click", pageClear);
    inputClearBtn.addEventListener("click", pageClear);
    // 검색어 입력창에 이벤트 리스너 추가
    searchInput.addEventListener("input", () => {
        const inputValue = searchInput.value.trim(); // 입력된 검색어
        if (inputValue.length >= 3) {
            filterMovies(inputValue);
            movieListHeading.textContent = "Filtered List";
        } else {
            showMovieCards();
            movieListHeading.textContent = "The Entire List";
        }
    });

    // 검색창 클리어 버튼 이벤트 처리 리스너
    function pageClear() {
        searchInput.value = "";
        showMovieCards();
        movieListHeading.textContent = "The Entire List";
    };
});

// 실시간 검색 기능 위에 있는 기존에 있던 코드에서 실시간으로 검색 결과가 표시되는 기능을 추가했고 입력한 글자의 길이가 3개 이상이면 표시되도록 했다.

이전에 방명록을 만들면서 방명록의 항목을 바로 실시간으로 표시하게 만드는 기능을 만들어서 어렵지 않게 만들 수 있었다.

 

 

4. 연도별 필터링 모달창 구현


<!-- 모달창 -->
    <div id="yearModal" class="modal">
        <div class="modal-content">
            <ul class="year-list">
                <li class="year-item" id="all">All</li>
                <li class="year-item" id="before1960">Before 1960</li>
                <li class="year-item" id="1960s">1960s</li>
                <li class="year-item" id="1970s">1970s</li>
                <li class="year-item" id="1980s">1980s</li>
                <li class="year-item" id="1990s">1990s</li>
                <li class="year-item" id="2000s">2000s</li>
                <li class="year-item" id="after2010">After 2010</li>
            </ul>
        </div>
    </div>
// "movie-list-header" 클릭 이벤트 리스너 추가
document.getElementById("movie-list-header").addEventListener("click", function () {
    // 모달 표시
    document.getElementById("yearModal").style.display = "block";
});

// 모달 영역 밖을 클릭하면 모달 닫기
window.onclick = function (event) {
    var modal = document.getElementById("yearModal");
    if (event.target == modal) {
        modal.style.display = "none";
    }
}
// 모달 내 연도 항목 클릭 이벤트 리스너 추가
document.getElementById("yearModal").addEventListener("click", function (event) {
    const target = event.target;
    if (target.classList.contains("year-item")) {
        const yearId = target.id;
        if (yearId === "all") {
            showMovieCards(); // "All"을 선택한 경우 모든 영화 카드 보이기
            document.getElementById("movie-list-heading").textContent = "The Entire List"; // 클릭된 항목의 id를 movie-list-heading의 텍스트로 설정
        } else {
            filterMoviesByYear(yearId);
            document.getElementById("movie-list-heading").textContent = target.textContent;
        }       
    }
});

모달 창은 블로그 만들 때 만들어봐서 어렵지 않게 구현했다

 

// 연도별 영화 필터링 함수
const filterMoviesByYear = (year) => {
    const movieCards = document.querySelectorAll(".movie-card");
    movieCards.forEach(movieCard => {
        if (movieCard.classList.contains(year)) {
            movieCard.style.display = "block";
        } else {
            movieCard.style.display = "none"; // 선택된 연도의 클래스가 없는 영화 카드는 숨김
        }
    });
};


// 영화 카드 생성 함수
const createMovieCard = (movie, index) => {
    const movieCard = document.createElement("div");
    movieCard.classList.add("movie-card");
    movieCard.id = `${movie.id}`;

    // 출시 연도에 따라 클래스 추가
    const releaseYear = parseInt(movie.release_date.split('-')[0]);
    if (releaseYear < 1960) {
        movieCard.classList.add("before1960");
    } else if (releaseYear < 1970) {
        movieCard.classList.add("1960s");
    } else if (releaseYear < 1980) {
        movieCard.classList.add("1970s");
    } else if (releaseYear < 1990) {
        movieCard.classList.add("1980s");
    } else if (releaseYear < 2000) {
        movieCard.classList.add("1990s");
    } else if (releaseYear < 2010) {
        movieCard.classList.add("2000s");
    } else {
        movieCard.classList.add("after2010");
    }

    const img = document.createElement("img");
    img.src = `https://image.tmdb.org/t/p/w500${movie.poster_path}`;
    img.alt = movie.title;

    const title = createMovieTitle(movie.title); // 영화 타이틀 요소 생성
    const content = createMovieContent(movie.overview); // 영화 소개 요소 생성
    const rating = createMovieRating(movie.vote_average); // 영화 평점 요소 생성
    const placeBadge = createPlaceBadge(index); // 영화 뱃지 생성

    movieCard.append(img, title, rating, content, placeBadge);
    return movieCard;
};

const releaseYear = parseInt(movie.release_date.split('-')[0]);에 카드의 해당 연도를 구해서 중첩 if문으로 class에 연도를 추가하고 모달창의 리스트에 연도를 선택하면 해당하는 클래스의 카드들의 display를 "block"으로 할당하니까 에러 없이 해결되었다.

 

반응형 웹 만들기


문제점

CSS를 이용해서 반응형 웹을 만드는 과정에서 개발자 도구의 기기 툴바를 사용하여 화면을 모바일 크기로 변경할 때 보니까 html의 width의 크기를 width=device-width로 설정했는데도 화면의 X축 스크롤바가 생기는 이상한 점을 발견했다.

 

화면에 나온 x축 스크롤바

 

<div class="toggle-wrap">
            <div class="toggle-container">                      
                <label for="light-mode-toggle" class="toggle-label">                             
                    <span class="toggle-text">어두운 모드</span>                
                    <span id="slider-label">
                        <input type="checkbox" id="light-mode-toggle" checked>
                        <span class="slider round"></span>
                    </span>
                </label>            
            </div>            
        </div>

html를 주석처리 하면서 문제가 발생한 곳을 추적해 보니 다크모드/라이트 모드 토글하는 곳에 문제가 있었고 

/* 다크 모드 버튼 */
.toggle-wrap {
    display: flex;
    justify-content: flex-end;
    margin-right: 10px;
}
.toggle-container {                
    padding: 20px;
    width: 200px;    
}

.toggle-label {
    display: flex;
    align-items: center;
}
.toggle-text {
    font-weight: 600;    
}

.toggle-text:hover {
    transform: scale(1.2);
    transition-duration: 0.5s;
}

#slider-label {
    position: relative;
    display: inline-block;
    width: 60px;
    height: 34px;
    margin-left: 10px;
}

#slider-label #light-mode-toggle {
    opacity: 0;
    width: 170px; // 잘못 설정한 부분 원래는 width가 있으면 안 됐었다.
    height: 0;
}

/* 슬라이더 CSS */

해당하는 css 부분에서. toggle-wrap 부분의 justify-content: flex-end;를 주석 처리하니 화면에 x축에 스크롤바가 사라진 것을 확인했고. "toggle-wrap 부분에 flex를 지우고 화면에 오른쪽에 표시하도록 overflow: hidden을 추가하고, toggle-container 클래스에 float: right;를 적용하여 문제를 해결했다.

하지만 왜 justify-content: flex-end;를 해서 width=device-width가 초과되었는지 이해가 안 가서 구글링으로 검색해 보았지만 정답을 찾을 수 없었다.

그래서 같은 조원분들한테 문제를 상의했고 해답을 찾았다.  바로 #slider-label #light-mode-toggle의 width가 설정되어 있던 것이다. 

slider-label와 #slider-label #light-mode-toggle 부분 아래에 슬라이더 css가 설정되어 있어서 초기에 css를 설정할 때 실수로. toggle-label가 #slider-label #light-mode-toggle인지 알고 width를 설정해 주었던 것 같다

 #slider-label #light-mo

de-toggle의 width를 크게 설정하고 투명도를 지우고 높이를 설정해 주니

숨어있던 버튼이 튀어나왔다. 결국 이 버튼 때문에 x축으로 스크롤이 발생했던 것이었다.

 

결론


다양한 기능들을 추가하면서 JS에서 만약 오류가 콘솔에 뜨면 해당하는 오류의 위치와 이유를 파악할 수 있었는데 CSS에서 실수는 오류도 거의 안 뜨고 오히려 JS보다 더 위험하다고 생각했다.

그리고 혼자 디버깅할 때 보지 못했던 나의 실수를 다른 분이 봐서 금방 찾으신 것을 보고 협업의 중요성을 느꼈고 다음에는 이와 같은 실수를 하지 않겠다고 다짐한다.