2차 공부/TIL

24.07.26 영화검색페이지 리팩토링하기

공대탈출 2024. 7. 26. 14:35

이러한 페이지를 만들었다.

영화데이터 제공 openapi를 사용하여 페이지 진입 시 영화데이터를 불러온다.

불러온 데이터 중 원하는 키워드를 검색하여 해당 키워드가 '제목'에 있는 영화들만 보이도록한다.

import { getMoviesFunc } from "./getMoviesFunc.js";
import { movieCardsFunc } from "./movieCardsFunc.js";
import { addEvent } from "./addEvent.js";
import { searchMovie } from "./searchMovie.js";
import { observer } from "./observer.js";

observer();

getMoviesFunc().then((res) => {
    movieCardsFunc(res.results).then(() => {
        addEvent();
    });
    searchMovie(res.results);
});

html에 script로 불러지는 app.js이다.

페이지가 로드되면 script가 실행되고, getMoviesFunc함수를 실행한다.

 

export async function getMoviesFunc() {
    const options = {
        method: "GET",
        headers: {
            accept: "application/json",
            Authorization:
                "Bearer Authorization",
        },
    };

    try {
        let response = await fetch(
            "https://api.themoviedb.org/3/movie/top_rated?language=en-US&page=1",
            options
        );
        return response.json();
    } catch (err) {
        throw err;
    }
}

해당 사이트에서 제공해주는 api명세서에 따라 try catch와 async await를 사용하여 fetch요청을하고, 받아온 데이터를 response로 받아 json형태로 반한다.

 

getMoviesFunc().then((res) => {
    movieCardsFunc(res.results).then(() => {
        addEvent();
    });
    searchMovie(res.results);
});

다시 app.js에서 .then을 사용하여 return으로 받은 response.json()의 results를 movieCardsFunc의 매개변수로 보내준다.

 

export async function movieCardsFunc(movieArr) {
    await movieArr.forEach((movie) => {
        const { id, title, overview, poster_path, vote_average } = movie;
        const cardList = document.querySelector(".cardList");
        let temp_html = `
                <div class="movieCard" id='${id}'>
                    <img
                        class="movieImage"
                        src="https://image.tmdb.org/t/p/w500${poster_path}"
                        alt="포스터 이미지"
                    />
                    <h3>${title}</h3>
                    <p>${overview}</p>
                    <p>Rating: <span>${vote_average}</span></p>
                </div>
            `;
        cardList.innerHTML += temp_html;
    });
}

받은 movieArr의 각 영화마다 카드를 만들어 cardList라는 element 안에 차례대로 더해준다.

 

export function addEvent() {
    let targetElements = document.querySelectorAll(".movieCard");
    targetElements.forEach((element) => {
        element.addEventListener("click", function () {
            alert(`movie id : ${element.id}`);
        });
    });
}

카드가 모두 더해지면 모든 movieCard에 클릭시 영화 id를 alert하도록 이벤트리스너를 달아준다.

 

export function searchMovie(movieArr) {
    const searchButton = document.querySelector(".searchButton");
    const searchInput = document.querySelector(".searchInput");
    const searchFunc = function () {
        const newMovieArr = movieArr.filter((el) =>
            el.title.toLowerCase().includes(searchInput.value.toLowerCase())
        );
        if (newMovieArr.length !== 0) {
            const cardList = document.querySelector(".cardList");
            cardList.innerHTML = "";
            newMovieArr.forEach((movie) => {
                const { id, title, overview, poster_path, vote_average } =
                    movie;
                const temp_html = `
                        <div class="movieCard" id='${id}'>
                            <img
                                class="movieImage"
                                src="https://image.tmdb.org/t/p/w500${poster_path}"
                                alt="포스터 이미지"
                            />
                            <h3>${title}</h3>
                            <p>${overview}</p>
                            <p>Rating: <span>${vote_average}</span></p>
                        </div>
                    `;
                cardList.innerHTML += temp_html;
            });
        } else {
            alert("검색어를 포함하는 영화가 존재하지 않습니다!");
            searchInput.value = "";
        }
    };
    searchButton.addEventListener("click", () => searchFunc());
    searchInput.addEventListener("keyup", (e) => {
        if (e.keyCode === 13) searchFunc();
    });
}

마지막으로 검색기능도 처음 사이트 진입시 api로 받아왔던 데이터를 받아 해당데이터 속 input의 value를 포함한 것들을 카드로 만들고, cardList를 비우고 cardList에 하나씩 더해주는 함수를 이벤트리스너로 달아주었다.

 


다 만들고 바닐라 자바스크립트로 무한스크롤을 이용하여 데이터를 계속해서 받아오는 작업을 진행했다.

 

<body>
    ...
    <div class="cardList"></div>

            <div class="temp"></div>
    <footer>
    ...
    </footer>
</body>

index.html의 footer위에 테스트용 타겟div를 만들어주고

 

// import { getMoviesByPageNum } from "./getMoviesByPageNum";

export function observer() {
    const $footer = document.querySelector(".footerText");
    const $temp = document.querySelector(".temp");
    // let pageNum = 2;
    // const options = {
    //     method: "GET",
    //     headers: {
    //         accept: "application/json",
    //         Authorization:
    //             "Bearer Authorization",
    //     },
    // };

    //옵저버 생성
    const io = new IntersectionObserver(
        async (entry, observer) => {
            //현재 보이는 타겟 출력
            const ioTarget = entry[0].target;

            //viewport에 target이 보인다면
            if (entry[0].isIntersecting) {
                //아래 명령을 실행해라
                console.log("현재 보이는 타겟", ioTarget);
                //현재 보이는 target감시 취소
                io.unobserve($footer);
                //어떤 작업을 진행한다.
                $temp.innerHTML += "가나다라";
                // getMoviesByPageNum();
                //다시 감시해
                io.observe($footer);
            }
        },
        //타겟이 50%이상 보이면 한다.
        { threshold: 0.5 }
    );

    //footer감시해
    io.observe($footer);
}

footer의 p태그가 보이면, 임시태그 temp에 글자를 추가하여 작동을 잘 하는지 확인하였다.

아주 잘 작동하였다.

 

이제 p태그가 보이면, pageNum에 따라 api서버에 데이터를 추가로 요청하고, 해당 데이터를 cardList 아래에 붙여주는 작업을 해야했다.


문제점

붙이는건 문제가 안된다. 서버에 pageNum에 따라 요청을하고, 해당 요청을 잘 받아왔다.

하지만 받아온 데이터는 검색기능의 데이터에 포함이 안되는 문제가 있었다.

 

생각해본 문제점

 

모든 데이터를 상위 store에 저장하고, 해당 데이터를 뿌려주고 검색하는 기능이 아니라, 페이지가 로드되면 데이터를 처음 받아오고, 해당 데이터에 따라 검색을 하는 함수였기 때문에 생긴 문제였다.

 

따라서 검색기능에 변화가 필요했다.

데이터를 받아와 검색하는 것이 아니라, 사용자가 입력한 키워드에따라 다시 서버에 요청하여 해당 데이터를 다시 뿌려주는 것으로 변경을 할 것이다.

그렇게 한다면 무한스크롤로 받아오는 데이터와 검색데이터가 분리되는 문제가 있긴하지만 전역상태관리를 바닐라 자바스크립트로 만들기엔 무리라 생각하여 일단 추후에 생각해볼 것이다.

검색버튼 클릭시 localStorage나 sessionStorage에 isSearched를 boolean값으로 저장하여 isSearched가 false일때만 무한스크롤이 작동하도록 하여도 괜찮을 것 같다.


검색기능 리팩토링

다행히도 키워드에따라 query에 넣어 데이터를 요청하는 api가 있었다!

 

export function searchMovie(movieArr) {
    const searchButton = document.querySelector(".searchButton");
    const searchInput = document.querySelector(".searchInput");
    const searchFunc = async function () {
        let movieArr = [];
        const options = {
            method: "GET",
            headers: {
                accept: "application/json",
                Authorization:
                    "Bearer Authorization",
            },
        };
        if (searchInput.value.length !== 0) {
            await fetch(
                `https://api.themoviedb.org/3/search/movie?query=${searchInput.value}&include_adult=false&language=en-US&page=1`,
                options
            )
                .then((response) => response.json())
                .then((response) => {
                    let results = response.results;
                    movieArr.push(...results);
                    console.log(movieArr);
                    if (movieArr.length !== 0) {
                        const cardList = document.querySelector(".cardList");
                        cardList.innerHTML = "";
                        movieArr.forEach((movie) => {
                            const {
                                id,
                                title,
                                overview,
                                poster_path,
                                vote_average,
                            } = movie;
                            const temp_html = `
                                    <div class="movieCard" id='${id}'>
                                        <img
                                            class="movieImage"
                                            src="https://image.tmdb.org/t/p/w500${poster_path}"
                                            alt="포스터 이미지"
                                        />
                                        <h3>${title}</h3>
                                        <p>${overview}</p>
                                        <p>Rating: <span>${vote_average}</span></p>
                                    </div>
                                `;
                            cardList.innerHTML += temp_html;
                        });
                    } else {
                        alert("검색어를 포함하는 영화가 존재하지 않습니다!");
                        searchInput.value = "";
                    }
                });
        } else {
            alert("검색어를 입력해주세요!");
        }
    };
    searchButton.addEventListener("click", () => searchFunc());
    searchInput.addEventListener("keyup", (e) => {
        if (e.keyCode === 13) searchFunc();
    });
}

input에 검색어를 입력하고 이벤트가 실행되면 검색값을 query로 api요청을 한다.

요청을통해 가져온 데이터를 가공하여 CardList를 비우고, 안에 하나씩 붙여준다.

만약 검색어로 데이터가 오지 않는다면 존재하지않는다는 alert를 준다.

검색어를 입력하지 않아도 alert를 내보낸다.


현재 검색과 메인페이지가 분리되어있지않아 무한스크롤을 구현했을 때 이상하게 데이터가 섞일수있는것은 안다.

하지만 페이지를 분리한다하더라도 일단 무한스크롤을 통해 데이터를 붙여주는걸 해보고자한다.

import { getMoviesByPageNum } from "./getMoviesByPageNum.js";

export function observer() {
    const $footer = document.querySelector(".footerText");
    let pageNum = 2;
    const options = {
        method: "GET",
        headers: {
            accept: "application/json",
            Authorization:
                "Bearer Authorization",
        },
    };

    //옵저버 생성
    const io = new IntersectionObserver(
        async (entry, observer) => {
            //현재 보이는 타겟 출력
            const ioTarget = entry[0].target;

            //viewport에 target이 보인다면
            if (entry[0].isIntersecting) {
                //아래 명령을 실행해라
                //현재 보이는 target감시 취소
                io.unobserve($footer);
                //어떤 작업을 진행한다.
                await fetch(`https://api.themoviedb.org/3/movie/top_rated?language=en-US&page=${pageNum}`, options)
                    .then((res) => res.json())
                    .then((res) => getMoviesByPageNum(res));
                pageNum++;

                //다시 감시해
                io.observe($footer);
            }
        },
        //타겟이 50%이상 보이면 한다.
        { threshold: 0.5 }
    );

    //footer감시해
    io.observe($footer);
}

target이 보이면 pageNum에따라 api요청을 보내고 promise로 받아온 res를 .then을 사용해서 가공한 뒤,

가공데이터를 .then을 사용해서 getMoviesByPageNum함수로 보내준다.

export function getMoviesByPageNum(res) {
    res.results.forEach((movie) => {
        const { id, title, overview, poster_path, vote_average } = movie;
        const cardList = document.querySelector(".cardList");
        let temp_html = `
            <div class="movieCard" id='${id}'>
                <img
                    class="movieImage"
                    src="https://image.tmdb.org/t/p/w500${poster_path}"
                    alt="포스터 이미지"
                />
                <h3>${title}</h3>
                <p>${overview}</p>
                <p>Rating: <span>${vote_average}</span></p>
            </div>
        `;
        cardList.innerHTML += temp_html;
    });
    return;
}

getMoviesByPageNum이 받은 값을 카드형태로 만들어 cardList의 뒤에 붙여주는 형식이다.

 


생각했던 문제가 발생했다.

검색 후 스크롤을 내리면 검색결과와 상관없는 데이터들이 pageNum에 의하여 추가되는 것이다.

그래서 강의에서 들었던 class의 getter와 setter를 이용하기로 했다.

class isSearched {
    constructor(bool) {
        this._isSearched = bool;
    }

    get isSearched() {
        return this._isSearched;
    }
    set isSearched(value) {
        this._isSearched = value;
    }
}

export let isSearchedData = new isSearched(false);

isSearched라는 클래스를 만들고 false로 시작하는 isSearchedData를 export해주었다.

import { isSearchedData } from "./isSearched.js";

export function searchMovie(movieArr) {
    console.log(isSearchedData._isSearched);
    const searchButton = document.querySelector(".searchButton");
    const searchInput = document.querySelector(".searchInput");
    const searchFunc = async function () {
        isSearchedData._isSearched = true;
        console.log(isSearchedData._isSearched);
        ...
    }
    ...
}

검색하는 함수에 isSearched의 setter을 이용하여 검색 시 true로 바뀌도록 하였다.

 

import { getMoviesByPageNum } from "./getMoviesByPageNum.js";
import { isSearchedData } from "./isSearched.js";

export function observer() {
	//...
            //viewport에 target이 보인다면
            if (entry[0].isIntersecting) {
                //아래 명령을 실행해라
                //현재 보이는 target감시 취소
                io.unobserve($footer);
                //어떤 작업을 진행한다.
                if (isSearchedData._isSearched) {
                    window.scrollTo({ top: 0, left: 0, behavior: "auto" });
                    alert("검색중에는 데이터를 더 불러올 수 없습니다!");
                } else {
                    await fetch(`https://api.themoviedb.org/3/movie/top_rated?language=en-US&page=${pageNum}`, options)
                        .then((res) => res.json())
                        .then((res) => getMoviesByPageNum(res));
                    pageNum++;
                }
			//...
			}
		//...
        }

observer의 타겟이 화면에 들어왔을때 isSearchedData를 get해서 false라면 데이터요청을, true라면 alert와 스크롤업을 구현했다.

 

 

피드백은 이러했다. 검색api에도 pageNum에 따른 검색이 가능하기때문에, 검색상태라면 검색데이터를 더 추가해주고, 검색상태가 아니라면 현재처럼 추가데이터를 받아오는 방식이었다.


class isSearched {
    constructor(bool) {
        this._isSearched = bool;
    }

    get isSearched() {
        return this._isSearched;
    }
    set isSearched(value) {
        this._isSearched = value;
    }
}

export let isSearchedData = new isSearched(false);

검색된 상황인지에 대한 변수를 제공받고, 변수를 변경할 수 있는 isSearchedData를 class를 이용하여 만들었다.

getter과 setter을 포함하고있어 외부에서 해당 값을 변경할 수 있다.

 

import { isSearchedData } from "./isSearched.js";

export function searchMovie(movieArr) {
    console.log(isSearchedData._isSearched);
    const searchButton = document.querySelector(".searchButton");
    const searchInput = document.querySelector(".searchInput");
    const searchFunc = async function () {
        isSearchedData._isSearched = true;
        console.log(isSearchedData._isSearched);
        ...
    }
}

검색기능 함수에서 isSearchedData의 setter을 이용하여 검색시 true로 값을 바꾼다.

 

import { getMoviesByPageNum } from "./getMoviesByPageNum.js";
import { getSearchedMoviesByPageNum } from "./getSearchedMoviesByPageNum.js";
import { isSearchedData } from "./isSearched.js";

export function observer() {
    const $footer = document.querySelector(".footerText");
    const searchInput = document.querySelector(".searchInput");
    let pageNum = 2;
    let searchedPageNum = 2;
    const options = {
        method: "GET",
        headers: {
            accept: "application/json",
            Authorization:
                "Bearer eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJiNTUxNjFmZmY2NzQ5ZmIyNmQ1Yzk5OGMzZDNlNzBjNiIsIm5iZiI6MTcyMTkwNjkxMi41ODE1NzUsInN1YiI6IjY2OWRhZWI2ZmZlYzQxZWExMzRiZDA2NiIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.k2yBax8nmYtDjpWNUWt1_JtMLi7dRGLMvgamTtiwvBY",
        },
    };

    //옵저버 생성
    const io = new IntersectionObserver(
        async (entry, observer) => {
            //현재 보이는 타겟 출력
            const ioTarget = entry[0].target;

            //viewport에 target이 보인다면
            if (entry[0].isIntersecting) {
                //아래 명령을 실행해라
                //현재 보이는 target감시 취소
                io.unobserve($footer);
                //어떤 작업을 진행한다.
                if (isSearchedData._isSearched) {
                    await fetch(
                        `https://api.themoviedb.org/3/search/movie?query=${searchInput.value}&include_adult=false&language=en-US&page=${searchedPageNum}`,
                        options
                    )
                        .then((res) => res.json())
                        .then((res) => getMoviesByPageNum(res));
                    searchedPageNum++;
                } else {
                    await fetch(`https://api.themoviedb.org/3/movie/top_rated?language=en-US&page=${pageNum}`, options)
                        .then((res) => res.json())
                        .then((res) => getMoviesByPageNum(res));
                    pageNum++;
                }

                //다시 감시해
                io.observe($footer);
            }
        },
        //타겟이 50%이상 보이면 한다.
        { threshold: 0.5 }
    );

    //footer감시해
    io.observe($footer);
}

observer에서 getter을 이용하여 데이터를 가져오면 검색시와 아닐때를 구분할 수 있어 검색시 검색키워드에 대한 무한스크롤이 작동하고, 일반 상태시 일반 무한스크롤이 작동하게 된다.

다음으론 처음 페이지 진입시 들어온 데이터중 포스터를 사용하여 페이지네이션을 만들어볼 예정이다.

'2차 공부 > TIL' 카테고리의 다른 글

24.07.29 NextJs 페이지 라우팅  (0) 2024.07.29
24.07.29 Nextjs로 리액트프로젝트 생성하기  (0) 2024.07.29
24.07.25 클로저  (0) 2024.07.25
24.07.25 클래스  (0) 2024.07.25
24.07.25 DOM / DOM 조작하기  (0) 2024.07.25