오늘의 알고리즘 풀이!
캠프에서 제공하는 문제 시트는 하루에 5개씩 풀기로 했다.
오늘은 5의 숫자에 맞추기 위해 조금 더 풂!
오늘 코테 기초 문제를 풀고 있는데, 문제를 풀고 났더니 너무 주먹구구식으로 푼 것 같고 코드를 개선할 여지가 있는 것 같아서, 코드의 가독성을 개선하고 의미가 명확해지도록 작성하는 연습을 해봤다.
오늘 푼 문제 중에서 하나를 예시로 해서 그 과정을 기록해본다.
문제 설명
문제 설명
- 머쓱이네 옷가게는 10만 원 이상 사면 5%, 30만 원 이상 사면 10%, 50만 원 이상 사면 20%를 할인해줍니다.
- 구매한 옷의 가격 price가 주어질 때, 지불해야 할 금액을 return 하도록 solution 함수를 완성해보세요.
제한사항
- 10 ≤ price ≤ 1,000,000
- price는 10원 단위로(1의 자리가 0) 주어집니다.
- 소수점 이하를 버린 정수를 return합니다.
price | result |
---|---|
150,000 | 142,500 |
580,000 | 464,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; }
단순히 문제를 많이 푸는 것도 보람차고 공부가 되지만, 이렇게 시간을 들여서 코드의 의도나 구성에 대해 고민하며 코드를 곱씹어보는 것도 즐거운 것 같다.
현재 입문 문제 도전중!
과제로 주어진 개인 프로젝트가 어제 마무리되기는 했지만 오늘 문제를 풀면서 코드를 조금 더 개선시킬 여지가 많이 남아있다는 생각이 들었다. 그래서 오늘은 코드 분리 를해보기로 했다. 코드 분리란 단어는 너무 거창한가?
그러면 "똥같은 코드에 생명을 불어넣기"...?
지금 보면 아래에 선언된 함수는 크게 두 가지인데
makeMovieArticle()
: 영화 카드를 필터링하거나 묶어서 body 태그로 내보낸다.
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);
};
두 번째 고민은 이거였는데, 전역 변수를 치우는 일이었다.
근데 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도 나름 활용해보고, 함수형 프로그래밍에 대해서도 고민해보며 리팩토링을 진행해보았다. 밖으로 노출되는 코드를 최소화시키고, 필요한 곳에서만 코드가 쓰일 수 있도록 최대한 정리했다. 뿌듯!