2차 공부/TIL

24.08.06 코드 리팩토링

공대탈출 2024. 8. 6. 23:13

저번이나 이번 프로젝트 둘 다 나는 html에 작성하던 script를 모듈화 하였다.

 

그럼 우리가 모듈화 하는 이유는 뭘까?

 

가장 중요한 첫번째 이유는 "코드의 재사용성"이다.

 

가령 우리가 a와 b를 더해주는 함수를 사용할 때마다 만든다고 가정해보자.

function a() {
	function add(a,b) {
    	return a+b
    }
    console.log(add(1,2))
}

function b() {
	function add(a,b) {
    	return a+b
    }
    console.log(add(1,2))
}

function c() {
	function add(a,b) {
    	return a+b
    }
    console.log(add(1,2))
}

한눈에 봐도 더러운 코드가 되고, 재사용성이 떨어지며, 유지보수가 어렵게 느껴진다.

여기서 add의 기준을 a + b + 1을 하는 행동으로 바꿔야한다면, 세 함수 모두 하드코딩해야 한다는 것이다.

 

따라서 우리는 add라는 함수를 따로 선언해두고, 필요한곳마다 호출하여 사용하기위해 모듈화를 하는 것이다.

function add(a,b) {
	return a+b+1
}

function a() {
    console.log(add(1,2))
}

function b() {
    console.log(add(1,2))
}

function c() {
    console.log(add(1,2))
}

얼마나 깔끔한가?

코드 줄수도 매우 줄었을 뿐더러 명확하게 어떤 행동을 하는지 알 수 있다.

 

따라서 우리는 모듈화를 결정할 때 제일 먼저, 해당 함수가 재사용되는지 확인한다.

그리고 그 함수들을 적당히 분류하여(api요청끼리, addEvent끼리, makeBox끼리...) 한 파일안에서 각각을 export해주는 것이다.

그런 함수뭉치파일에서 원하는 함수만 적절한 때 import하여 사용하는것이 올바르다.

 

또한 그렇게 함수자체를 만들어두면 위처럼 어떤 동작을 동일하게 변경해야할 때 처리하기도 편하다.

 

두번째로 한 함수내에서 모듈화를 하는 것은

export async function makeDetailBox(response) {
    const $detailContainer = document.querySelector(".detailContainer");
    const { adult, genres, id, overview, release_date, runtime, title, vote_average, poster_path, backdrop_path } =
        response;
    
    $detailContainer.innerHTML = `
    	<div>
        	<div>${adult}</div>
        	<div>${genres}</div>
        	<div>${id}</div>
        	<div>${overview}</div>
        	<div>${release_date}</div>
        	<div>${runtime}</div>
        	<div>${vote_average}</div>
        </div>
    `
}

이런 디테일컨테이너에 어떤 변수들을 적절히 넣는 작업을 한다고 가정해보자.

여기서는 코드가 짧고 템플릿 리터럴의 모양이 간결하여 괜찮지만, 현실은 그렇지않다.

div태그안에 p태그안에 span태그안에 title을 넣어주고, div태그안에 p태그안에 genres를 넣어주고.....

를 반복하다보면 저 템플릿리터럴 속 요소들이 정말 보기힘들정도로 더러워진다.

 

따라서 우리는 저 innerHTML을 지정해주는 것을 모듈화 하여 코드의 직관성을 높여야할 필요가 있다.

이것이 모듈화의 두번째 필요성이다.

function setDetailContainer(adult, genres, id, overview, release_date, runtime, vote_average) {
	$detailContainer.innerHTML = `
    	<div>
        	<div>${adult}</div>
        	<div>${genres}</div>
        	<div>${id}</div>
        	<div>${overview}</div>
        	<div>${release_date}</div>
        	<div>${runtime}</div>
        	<div>${vote_average}</div>
        </div>
    `
}

export async function makeDetailBox(response) {
    const $detailContainer = document.querySelector(".detailContainer");
    const { adult, genres, id, overview, release_date, runtime, title, vote_average, poster_path, backdrop_path } =
        response;
    
    setDetailContainer($detailContainer, adult, genres, id, overview, release_date, runtime, vote_average)
}

이렇게 makeDetailBox가 간결하고 어떤 행동을 하는지 직관적으로 파악이 가능해진다.

다만 어디서나 그렇지만, 모듈화한 함수의 네이밍을 정말 잘해야한다.

여기서처럼 setDetailContainer을 보면 아~ 디테일컨테이너를 세팅하는 함수구나, 이런 인자들이 들어가는구나 하며 어느정도 느낌이 온다.

그리고 자세히 알고싶으면 해당 함수를 보고 어떻게 동작하는건지 알면 되는 것이다.

 

 

아래 코드가 오늘 모듈화를 진행했던 코드중 하나이다.

import { getActorList, getMovieTrailer, getSimillarMovies } from "./getApiRequests.js";
import { getStarWidth, getGenresString, getruntime } from "./util.js";
import { YOUTUBE_API_KEY } from "../apiOptions.js";

//예고편을 추가하는 함수
function addTrailerContainer(trailerKey, trailerContainer) {
    //예고편을 담을 iframe 생성
    const $trailer = document.createElement("iframe");
    //iframe 속성 설정
    Object.assign($trailer, {
        id: `${trailerKey}`,
        style: "transition: all 0s linear;",
        src: `https://www.youtube.com/embed/${trailerKey}`,
        title: "YouTube video player",
        frameborder: "0",
        allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen"
    });
    //마우스 오버 이벤트를 작동시킬 최소너비 지정
    const responsiveWidth = matchMedia("screen and (min-width: 1500px)");
    //마우스 오버시 크기 변화
    $trailer.addEventListener("mouseover", () => {
        //최소너비 이상일때만 작동
        if (responsiveWidth.matches) {
            $trailer.style.transform = "scale(1.1)";
        }
    });
    //마우스 아웃시 크기 원래대로
    $trailer.addEventListener("mouseout", () => {
        $trailer.style.transform = "scale(1.0)";
    });
    //예고편을 담을 요소에 추가
    trailerContainer.appendChild($trailer);
}

function setDocumentTitle(title) {
    document.title = title;
}

function setBackgroundPoster(backdrop_path) {
    const $backgroundPoster = document.getElementById("backgroundPoster");
    Object.assign($backgroundPoster.style, {
        backgroundImage: `url(https://image.tmdb.org/t/p/w500${backdrop_path})`,
        backgroundSize: "cover",
        backgroundPosition: "top",
        filter: "blur(15px)",
        width: "calc(100vw - 17px)",
        height: "100vh",
        position: "absolute",
        zIndex: "-1"
    });
}

function setPoster(poster_path) {
    //포스터 이미지 생성
    const $moviePoster = document.createElement("img");
    $moviePoster.className = "moviePoster";
    //포스터 이미지가 서버에 있을경우 포스터 이미지를 불러오고 없을경우 대체이미지를 불러옴
    $moviePoster.src = poster_path
        ? `https://image.tmdb.org/t/p/w500${poster_path}`
        : "../../Asset/posterPlaceholder.png";
    return $moviePoster;
}

function setMovieDetailBox($movieDetailBox, title, starWidth, release_date, adult, runtime, genresString, overview) {
    $movieDetailBox.innerHTML += `
                        <h1 class="movieTitle">${title}</h1>
                        <div class="movieVote">
                        <p class='blankStar'>☆☆☆☆☆</p><p style='width: ${starWidth}%' class='filledStar'>★★★★★</p>
                        </div>
                        <p class="movieDesc">
                            <span class="movieReleaseDate">${release_date.slice(
                                0,
                                4
                            )} </span> / <span class="movieRating">${adult ? "19+" : "15+"}</span> /
                            <span class="movieRunningTime">${getruntime(runtime)}</span> /
                            <span class="moviegenres">${genresString}</span>
                        </p>
                        <p class="movieOverview">
                            ${overview}
                        </p>`;
}

// 디테일 박스를 만드는 함수
export async function makeDetailBox(response) {
    const $detailContainer = document.querySelector(".detailContainer");
    //영화 상세정보에서 필요한 정보 추출
    const { adult, genres, id, overview, release_date, runtime, title, vote_average, poster_path, backdrop_path } =
        response;
    //페이지 타이틀 변경
    setDocumentTitle(title);
    //배경 이미지 처리
    setBackgroundPoster(backdrop_path);

    //장르 배열 문자열화
    const genresString = getGenresString(genres);
    //평점에 따라 별이 채워지도록 계산
    const starWidth = getStarWidth(vote_average);

    //상세정보를 담을 요소 생성
    const $detailBox = document.createElement("div");
    $detailBox.className = "detailBox";
    //포스터 이미지를 상세정보를 담아줄 요소에 추가
    $detailBox.appendChild(setPoster(poster_path));
    //영화 상세정보를 담아줄 요소 생성
    const $movieDetailBox = document.createElement("div");
    $movieDetailBox.className = "movieDetailBox";
    //영화 상세정보를 상세정보를 담아줄 요소에 추가
    $detailBox.appendChild($movieDetailBox);
    //영화 상세정보를 적당한 템플릿 리터럴 규격에 맞춰 추가
    setMovieDetailBox($movieDetailBox, title, starWidth, release_date, adult, runtime, genresString, overview);
    //상세정보를 담아줄 요소에 추가
    $detailContainer.appendChild($detailBox);

    //예고편을 담아줄 요소 생성
    let $trailerContainer = document.createElement("div");
    //예고편을 담아줄 요소 스타일 지정
    $trailerContainer.className = "trailerContainer";
    $trailerContainer.id = "trailerContainer";
    Object.assign($trailerContainer.style, {
        marginTop: "10px",
        display: "flex",
        flexDirection: "row",
        gap: "5px",
        width: "inherit",
        padding: "10px 0px",
        overflowX: "auto"
    });

    //예고편 데이터를 getMovieTrailer함수를 통해 받아옴
    let trailerData = await getMovieTrailer(id);
    //예고편 데이터 중 타입이 Trailer인 것만 필터링
    trailerData = trailerData.filter((item) => item.type === "Trailer");
    //예고편 데이터가 있을경우 예고편을 추가
    if (trailerData.length > 0) {
        //예고편 데이터를 순회하며 예고편을 추가
        trailerData.forEach(async (trailer) => {
            //예고편의 키값 추출
            const trailerKey = trailer.key;
            //예고편의 키값과 youtubeAPIKEY를 이용해 youtubeAPI에 상세정보 요청할 주소 생성
            const YOUTUBE_API_URL = `https://www.googleapis.com/youtube/v3/videos?id=${trailerKey}&key=${YOUTUBE_API_KEY}&part=contentDetails,status`;
            //상세정보 요청
            await fetch(YOUTUBE_API_URL)
                .then((res) => res.json())
                .then((data) => {
                    //예고편의 임베드가능여부, 공개여부, 지역제한여부 확인
                    const { embeddable, privacyStatus } = data.items[0].status;
                    const { regionRestriction } = data.items[0].contentDetails;
                    let regionFlag = true;
                    //지역제한이 있고 한국이 허용지역에 없을경우 예고편 추가하지 않음
                    if (regionRestriction && !regionRestriction.allowed.some((el) => el === "KR")) {
                        regionFlag = false;
                    }
                    //임베드가능여부가 true이고 공개여부가 public이며 지역제한이 없을경우 예고편 추가
                    if (embeddable === true && privacyStatus === "public" && regionFlag) {
                        //예고편을 추가하는 함수 호출
                        addTrailerContainer(trailerKey, $trailerContainer);
                    }
                });
        });
        //예고편을 담아줄 요소를 상세정보를 담아줄 요소에 추가
        $movieDetailBox.appendChild($trailerContainer);
    }
    //예고편 로딩이 끝나면 로딩화면을 숨김
    const $dotWrapper = document.querySelector(".dotWrapper");
    $dotWrapper.setAttribute("style", "display: none");

    //로딩이 끝나면 댓글과 등장인물탭이 나타나도록
    const $commentContainer = document.getElementById("commentContainer");
    $commentContainer.style.display = "flex";
    const $actorList = document.getElementById("actorList");
    $actorList.style.display = "flex";

    //비슷한 영화를 불러오는 함수 호출
    getSimillarMovies(title);
    //배우 목록을 불러오는 함수 호출
    getActorList(title);
}

모듈화를 해서 나눠도 이정도 길이인데, 아니었을때 makeDetailBox함수가 얼마나 길었고 얼마나 보기 흉했을지 상상이라도 가는지

정말 눈뜨고 볼 수 없는 더러움이었다.

10년동안 제자리에있던 책장을 옮겼을 때 쌓여있던 먼지마냥 더러웠다.

그나마 깨끗해진것이다...

 

어쨋든 모듈화를 진행할 때는 

1. 코드의 재사용이 있는가?

2. 한 파일에서 특정 함수를 직관적으로 간결하게 표현할 필요가 있는가?

를 생각하여 모듈화를 진행해야한다. 안그러면 정말 남이 보기도 힘들지만 내가 해석하는데도 오래걸린다.