[TIL] 개인 프로젝트 코드 분리

곽재훈·2024년 4월 26일
3
post-thumbnail

여는 글

1. 알고리즘 풀이 진행 현황

오늘의 알고리즘 풀이!

캠프에서 제공하는 문제 시트는 하루에 5개씩 풀기로 했다.
오늘은 5의 숫자에 맞추기 위해 조금 더 풂!

코드 고치기.

오늘 코테 기초 문제를 풀고 있는데, 문제를 풀고 났더니 너무 주먹구구식으로 푼 것 같고 코드를 개선할 여지가 있는 것 같아서, 코드의 가독성을 개선하고 의미가 명확해지도록 작성하는 연습을 해봤다.

오늘 푼 문제 중에서 하나를 예시로 해서 그 과정을 기록해본다.

문제 설명

문제 설명

  • 머쓱이네 옷가게는 10만 원 이상 사면 5%, 30만 원 이상 사면 10%, 50만 원 이상 사면 20%를 할인해줍니다.
  • 구매한 옷의 가격 price가 주어질 때, 지불해야 할 금액을 return 하도록 solution 함수를 완성해보세요.

제한사항

  • 10 ≤ price ≤ 1,000,000
  • price는 10원 단위로(1의 자리가 0) 주어집니다.
  • 소수점 이하를 버린 정수를 return합니다.
priceresult
150,000142,500
580,000464,000

입출력 예 설명


입출력 예 #1
150,000원에서 5%를 할인한 142,500원을 return 합니다.


입출력 예 #2
580,000원에서 20%를 할인한 464,000원을 return 합니다.

처음에는 switch case문을 쓰려고 했는데 switch문에서 case에 범위를 포함할 수는 없어서 if문으로 교체했다.

함수는 price를 입력 받고 price의 범위에 따라서 할인율을 적용한 가격을 리턴한다.

const solution = price => {
    if(500000 <= price) {
        return Math.floor(price * 0.8);
    } else if (300000 <= price) {
        return Math.floor(price * 0.9);
    } else if (100000 <= price) { 
        return Math.floor(price * 0.95);
    } else {
        return price;
    }
}

다만 지금 보면 if문 안에 중복되는 코드들이 있어서 그걸 먼저 if문 밖으로 빼주기로 했다.

const solution = price => {
    const discount = rate => Math.floor(price * (100 - rate)/100);
    if(500000 <= price) {
        return discount(20);
    } else if (300000 <= price) {
        return discount(10);
    } else if (100000 <= price) { 
        return discount(5);
    } else {
        return price;
    }
}

discount(rate) 함수는 할인율을 rate이라는 매개변수로 받고 price에서 입력받은 rate의 퍼센트만큼 할인한 가격을 리턴해준다. 그래서 이제 if문 안에는 discount함수와 할인율만 입력하면 된다.

또한 할인된 가격을 계산하는 코드도 변경했는데,
그 이유는 기존처럼 discount에 0.8처럼 인자로 필요한 값을 입력받아 price에 곱하면 당장은 편하겠지만 이후에 누군가 discount(0.8);라는 코드를 본다면 price에 0.8을 곱하는 게 아니라 0.8% 할인된다는 것으로 인식할 수도 있지 않을까 라는 생각이 들었다.

즉, 매개변수의 역할이 직관적으로 이해되지 않는달까. 그래서 할인율 자체를 매개변수로 받고, 그 이후에 코드 내에서 다시 할인율을 계산하는 방법을 썼다.

기존 코드

const discount = rate => Math.floor(price * rate);
if(500000 <= price) {
        return discount(0.8);
// discount(0.8)은 20% 할인된 가격을 return하는 코드지만
// 이걸 보고 누가 그렇게 이해할까. 아마도 0.8% 할인이라고 생각할 듯.

다시 작성한 코드

const discount = rate => Math.floor(price * (100 - rate)/100);
if(500000 <= price) {
        return discount(20);
// 직관적으로 이해할 수 있도록 변경!

마지막으로 if문 마다 return문이 있는 것이 보기 좀 그래서 삼항 연산자를 연결하여 return을 하나만 쓸 수 있도록 코드를 간소화했다. 보기도 훨 편한 것 같다.

기존 코드

const solution = price => {
    if(500000 <= price) {
        return Math.floor(price * 0.8);
    } else if (300000 <= price) {
        return Math.floor(price * 0.9);
    } else if (100000 <= price) { 
        return Math.floor(price * 0.95);
    } else {
        return price;
    }
}

다시 작성한 코드

const solution = price => {
    const discount = rate => Math.floor(price * (100 - rate)/100);
    return 500000 <= price ? discount(20) :
    300000 <= price ? discount(10) :
    100000 <= price ? discount(5) : price;
}

단순히 문제를 많이 푸는 것도 보람차고 공부가 되지만, 이렇게 시간을 들여서 코드의 의도나 구성에 대해 고민하며 코드를 곱씹어보는 것도 즐거운 것 같다.


현재 입문 문제 도전중!

2. 개인 프로젝트 코드 분리

과제로 주어진 개인 프로젝트가 어제 마무리되기는 했지만 오늘 문제를 풀면서 코드를 조금 더 개선시킬 여지가 많이 남아있다는 생각이 들었다. 그래서 오늘은 코드 분리 를해보기로 했다. 코드 분리란 단어는 너무 거창한가?
그러면 "똥같은 코드에 생명을 불어넣기"...?

1) 영화 카드 만드는 코드 개선하기

기존 코드

지금 보면 아래에 선언된 함수는 크게 두 가지인데

  1. makeMovieArticle()
    : 영화 카드를 필터링하거나 묶어서 body 태그로 내보낸다.

  2. makeMovieCard()
    : 영화 데이터를 받아서 각각의 정보들을 HTML 태그로 가공하여 반환한다.

이렇게 두 가지가 있다. 그런데 문제는 지금 두 개의 함수 안에서 작성된 코드가 너무 많다. 즉, 보기에도 좋지 않고 함수의 역할도 잘 나뉘어 있다고 보기 힘든 상태이다. 그래서 기능별로 함수를 나누고 makeMovieArticle()makeMovieCard()를 다이어트시키는 작업을 오늘 진행했다.

// 영화 카드를 담은 ul을 만들어 body에 추가.
const makeMovieArticle = async () => {
  let movies = await getMovies("/movie/popular");
  if(filteringText) {
    movies = movies.filter(movie => {
      // console.log(movie)
      // console.log(movie.title)
      // console.log(filteringText)
      const movieTitle = movie.title.toUpperCase();
      return movieTitle.includes(filteringText.toUpperCase());
    })
  }
  if(filteringMode === "higher-rating") {
  movies.sort((prev, next) => {
    return next.voteAverage - prev.voteAverage;
  })    
  } else if (filteringMode === "lower-rating") {
    movies.sort((prev, next) => {
      return prev.voteAverage - next.voteAverage;
    })
  } else if (filteringMode === "newer") {
    movies.sort((prev, next) => {
      return new Date(next.releaseDate) - new Date(prev.releaseDate);
    })
  } else if (filteringMode === "older") {
    movies.sort((prev, next) => {
      return new Date(prev.releaseDate) - new Date(next.releaseDate);
    })
  }
  const isContent = document.querySelector(".content-box ul");
  if(isContent) {
    movieArticle.removeChild(isContent);
  }
  const ul = document.createElement('ul');
  let moviesHTML = movies.map(movie => makeMovieCard(movie));
  
  
  moviesHTML.forEach((movie, i) => {
    if(i<4 && !filteringText && !filteringMode) {
      movie.classList.remove('movie-card');
      movie.classList.add('hot-movie-card');
      const ratingWidth = parseInt(movie.childNodes[1].childNodes[2].style.width);
      movie.childNodes[1].childNodes[2].style.width = `${ratingWidth * 2}px`;
    } 
    ul.appendChild(movie);
  });
  movieArticle.appendChild(ul);
}

//각각의 영화 카드를 만드는 함수
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;
  const releaseDate = movie.releaseDate;
  // 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`;

  const releaseDateNode = makeParagraphNode(releaseDate);
  releaseDateNode.classList.add('release-date');

  infoNode.appendChild(overviewNode);
  infoNode.appendChild(ratingScoreNode);
  infoNode.appendChild(releaseDateNode);
  

  li.appendChild(infoNode);
  return li;
}

수정된 코드

코드의 양이 조금 더 늘어난 것 같기는 하지만, 확실히 코드의 기능 별로 분리되어 있어서 기능을 추가하거나 수정할 때 훨씬 편해진 모습이다.
makeMovieArticle()makeMovieCard()가 가벼워진게 확실히 좋다!

const filteringMovies = (filteringText, movies) => {
  if (filteringText) {
    return movies.filter((movie) => {
      const movieTitle = movie.title.toUpperCase();
      return movieTitle.includes(filteringText.toUpperCase());
    });
  } else {
    return movies;
  }
};
const sortingMovies = (sortingMode, movies) => {
  if (sortingMode) {
    switch (sortingMode) {
      case "higher-rating":
        return movies.sort((prev, next) => {
          return next.voteAverage - prev.voteAverage;
        });
      case "lower-rating":
        return movies.sort((prev, next) => {
          return prev.voteAverage - next.voteAverage;
        });
      case "newer":
        return movies.sort((prev, next) => {
          return new Date(next.releaseDate) - new Date(prev.releaseDate);
        });
      case "older":
        return movies.sort((prev, next) => {
          return new Date(prev.releaseDate) - new Date(next.releaseDate);
        });
    }
  } else {
    return movies;
  }
};
const deleteExistCards = () => {
  const isContent = document.querySelector(".content-box ul");
  if (isContent) {
    movieArticle.removeChild(isContent);
  }
};
const appendMovieCards = (movies) => {
  const ul = document.createElement("ul");
  let moviesCards = movies.map((movie) => makeMovieCard(movie));

  moviesCards.forEach((movieCard, i) => {

    if (i < 4 && !filteringText && !sortingMode) {
      movieCard.classList.remove("movie-card");
      movieCard.classList.add("hot-movie-card");

      //movie 카드에서 ratingScore노드 찾는 방법 연구.
      const ratingWidth = parseInt(
        movieCard.childNodes[1].childNodes[2].style.width
      );
      movieCard.childNodes[1].childNodes[2].style.width = `${ratingWidth * 2}px`;
    }
    ul.appendChild(movieCard);
  });
  movieArticle.appendChild(ul);
};
const makeParagraphNode = (text) => {
  const pTag = document.createElement("p");
  const content = document.createTextNode(text);
  pTag.appendChild(content);
  return pTag;
};
const makeLiTagNode = (id) => {
  const li = document.createElement("li");
  li.classList.add("movie-card");
  li.dataset._id = id;
  return li;
};
const makePosterNode = (posterPath) => {
  const posterNode = document.createElement("div");
  posterNode.classList.add("movie-poster");
  posterNode.style.backgroundImage = `url(${imageEndPoint + posterPath})`;
  return posterNode;
};
const makeInfoNode = (
  titleNode,
  originalTitleNode,
  overviewNode,
  ratingScoreNode,
  releaseDateNode
) => {
  const infoNode = document.createElement("div");
  infoNode.classList.add("movie-info");

  if (!originalTitleNode) {
    infoNode.append(titleNode, overviewNode, ratingScoreNode, releaseDateNode);
  } else {
    infoNode.append(
      titleNode,
      originalTitleNode,
      overviewNode,
      ratingScoreNode,
      releaseDateNode
    );
  }
  return infoNode;
};
const makeRatingScoreNode = (voteAverage) => {
  const ratingScoreNode = document.createElement("div");
  ratingScoreNode.classList.add("rating-score");
  ratingScoreNode.style.width = `${Math.floor(voteAverage) * 25}px`;
  return ratingScoreNode;
};
const makeNormalNode = (content, className) => {
  const node = makeParagraphNode(content);
  node.classList.add(className);
  return node;
};
const makeMovieCard = (movie) => {
  const {
    id,
    title,
    originalTitle,
    overview,
    posterPath,
    voteAverage,
    releaseDate,
  } = movie;
  
  let originalTitleNode;
  if (movie.title !== movie.originalTitle) {
    originalTitleNode = makeNormalNode(originalTitle, "movie-original-title");
  }

  const infoNode = makeInfoNode(
    makeNormalNode(title, "movie-title"),
    originalTitleNode,
    makeNormalNode(overview, "overview"),
    makeRatingScoreNode(voteAverage),
    makeNormalNode(releaseDate, "release-date")
  );

  const li = makeLiTagNode(id);
  li.appendChild(makePosterNode(posterPath));
  li.appendChild(infoNode);

  return li;
};

const makeMovieArticle = async () => {
  deleteExistCards();
  let movies = await getMovies("/movie/popular");
  movies = filteringMovies(filteringText, movies);
  movies = sortingMovies(sortingMode, movies);
  appendMovieCards(movies);
};

2) 전역 변수 치우기

두 번째 고민은 이거였는데, 전역 변수를 치우는 일이었다.
근데 filteringText랑 sortingMode등의 변수는 여기서 치우는 게 어려운 것이,

filterSearchBar.addEventListener("submit", (e) => {
  e.preventDefault();
  filteringText = filterTextArea.value;
  if (pattern_kor.test(filteringText)) {
    alert("정확한 단어를 입력해주세요!");
    filterTextArea.value = "";
    return;
  }
  play();
});

const play = async () => {
  await makeMovieArticle();
  addBtnEvent();
};

const makeMovieArticle = async () => {
  deleteExistCards();
  let movies = await getMovies("/movie/popular");
  movies = filteringMovies(filteringText, movies);
  movies = sortingMovies(sortingMode, movies);
  appendMovieCards(movies);
};

코드 맨 위에서 "submit" event가 발생하면 play() 함수가 실행되고,
play() 함수는 다시 makeMovieArticle()을 실행시키고,
makeMovieArticle()는 다시 filteringMovies()를 실행시키면서 filteringMovies(filteringText, movies)의 형태로
filteringText 자리에 사용자가 입력한 검색어를 인자로 전달한다.

즉, 사용자가 검색한 텍스트 데이터가 "submit" event부터 filteringMovies()까지 타고 내려와야했다.

근데 함수 스코프의 특징상, 이벤트에 대한 정보는 이벤트에 대한 함수인 맨 상위 이벤트에서 발생하고 그 밖으로 벗어나지 못한다. 그래서 다음 함수에 전달하려면 매개변수의 형태로 전달해야 하는데,

만약 이벤트 발생시 이걸 전역변수가 아니라 이벤트가 발생하는 함수 내에서 처리하려면 서너번의 함수가 실행될 때 모두 filteringText를 받아올 수 있도록 매개변수로 추가해줘야 했다. 근데 문제는 검색 기능뿐만 아니라 그 밑에 정렬 기능도 같은 알고리즘으로 동작한다는 것. 함수마다 매개변수를 두 개씩 새로 추가하라니 상상만 해도 끔찍하다.

그래서 나한테는 전역변수는 아니지만 전역변수처럼 스코프의 제한을 받지 않고 사용할 수 있는 무언가가 필요했다.

그래서 내가 찾은 일련의 해답은 class를 이용하는 것이었다.

class Filter {
  static pattern_kor = /[ㄱ-ㅎ|ㅏ-ㅣ]/;
  static #filter = "";
  static #sortOrder = "";
  static getFilter (){
    return this.#filter;
  }
  static setFilter (text){
    if (this.pattern_kor.test(text)) {
      alert("정확한 단어를 입력해주세요!");
      filterTextArea.value = "";
      return;
    } else {
      this.#filter = text;
    }
  }
  static getSortOrder () {
    return this.#sortOrder;
  }
  static setSortOrder (text){
    this.#sortOrder = text;
  } 
}

특히 필터는 사용자가 검색어를 입력할 때 올바르지 않은 형태의 단어를 입력하면 그걸 걸러주는 유효성 검사 기능이 필요했는데 getter와 setter를 활용하면서 코드가 조금 더 깔끔해졌다.

이렇게 새로 배운 class도 나름 활용해보고, 함수형 프로그래밍에 대해서도 고민해보며 리팩토링을 진행해보았다. 밖으로 노출되는 코드를 최소화시키고, 필요한 곳에서만 코드가 쓰일 수 있도록 최대한 정리했다. 뿌듯!

profile
개발하고 싶은 국문과 머시기

0개의 댓글

관련 채용 정보