stopWatch 회고

1. 복잡한 삼항 연산자를 메서드로 간단하게

1
2
3
4
5
6
const formatElapsedTime = (() => {
// 1 => '01', 10 => '10'
const format = (n) => (n + "").padStart(2, 0);
// const format = n => (n < 10 ? '0' + n : n + '');
return ({ mm, ss, ms }) => `${format(mm)}:${format(ss)}:${format(ms)}`;
})();
  • 삼항 연산자로 조건에 따라 문자열을 직접 넣어서 구현하였는데 padStart 메서드를 사용하면 가독성을 키웠다.
  • 리팩터링 전에는 minutes, seconds 같은 변수를 사용했는데, format 함수를 사용하여 중복을 제거해주었다.

2. 일관성있는 코드를 작성하자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const renderLaps = (() => {
const $laps = document.querySelector('.stopwatch > .laps');

// 랩 타임을 생성하고 DOM에 반영한다.
const createLapElement = (newLap, index) => {
const $fragment = document.createDocumentFragment();

const $index = document.createElement('div');
$index.textContent = index;
$fragment.appendChild($index);

const $newLab = document.createElement('div');
$newLab.textContent = formatElapsedTime(newLap);
$fragment.appendChild($newLab);

$laps.appendChild($fragment);

$laps.style.display = 'grid';
};
  • 리팩터링 전에는 addLap 함수에서 insertAdjacentHTML 메서드 사용하고 reset 함수에서는 innerHTML을 사용하여 일관성이 떨어졌다.

  • 위 코드는 fragment 라는 서브 DOM을 구성하여 기존 DOM을 추가하는 용도로 사용하여 일관성을 갖췄다.

3. 템플릿 건드리지 말라면 건드리지 말기

1
2
3
4
5
6
7
8
9
10
11
12
13
// 리팩터링 전
$laps.innerHTML = `
<div class="lap-title">Laps</div>
<div class="lap-title">Time</div>
`;

// 리팩터링 후
const removeAllLapElement = () => {
document
.querySelectorAll(".laps > div:not(.lap-title)")
.forEach(($lap) => $lap.remove());
$laps.style.display = "none";
};
  • 요구사항에 맞게 템플릿을 건드리지 않아야한다면 템플릿을 건드리지 않아야 하므로 innerHTML로 새로운 값을 할당하는 대신, 자식 노드에 반복문을 사용하였다.

Tabs 회고

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 리팩터링 전
window.addEventListener("DOMContentLoaded", init);

$tabs.addEventListener("click", (e) => {
const $navItems = [...document.querySelectorAll(".tab-content")];
if (!e.target.matches(".tab")) return;

activateTab(e, $navItems);
});

// 리팩터링 후
document.querySelector("nav").addEventListener("click", (e) => {
if (!e.target.classList.contains("tab")) return;

currentTabIndex = +e.target.dataset.index;

document.querySelector(".glider").style.transform = `translate3D(${
currentTabIndex * 100
}%, 0, 0)`;
document.querySelectorAll(".tab-content").forEach(($tabContent, i) => {
$tabContent.classList.toggle("active", i === currentTabIndex);
});
});
  • async init 함수 내부에서 $tabs 이벤트 핸들러 등록까지 함께 해줘서 응집도 높은 코드를 설계할 수 있다.

autocomplete 회고

1. 정규표현식에 콜백함수 사용

1
2
3
4
5
6
7
8
// 리팩터링 전
country.replace(regExp, `<strong>${country.match(regExp)}</strong>`);

// 리팩터링 후
name.replace(
new RegExp(`(${searchString})`, "i"),
(matched) => `<strong>${matched}</strong>`
);

2. 요구사항을 잘 읽고 적합하게 설계하자.

1
2
3
4
5
6
7
8
9
10
const selectSuggestItem = ($selectedItem) => {
// prettier-ignore
$toggleButton.innerHTML = ($selectedItem
? $selectedItem.innerHTML
: `<span>
<i class="bx bx-search"></i>
<span class="autocomplete-title">Select a country</span>
</span>`
) + `<i class="bx bx-caret-down"></i>`;
};
  • 리팩터링 이전 코드는 li 요소 안의 span 요소를 클릭하면 제대로 동작하지 않았다. 그래서 위와 같이 로직을 수정을 해줬어야 했다. 그러므로 꼭 요구사항을 적절히 읽고 그에 적절한 코드를 작성하는 습관을 기르자.

Carousel 회고

1. transitionend 이벤트가 발생했다 안하는 현상

1
2
3
4
5
6
$container.addEventListener("transitionend", carouselSlider.completeTransition);

$container.addEventListener(
"transitioncancel",
carouselSlider.completeTransition
);
  • transition 이벤트가 완료 되었을 때, 이벤트 핸들러를 호출하도록 코드를 작성하였다.
    그런데 transitionend 이벤트가 발생하기 전 즉, transition이 완료되기 전에 transition-property가 제거되거나 display:none 으로 설정이 변경되면 이벤트가 생성되지 않는다.

그래서 보통은 안전하게 CSS 속성 변화가 되었을 때, transitionend 이벤트를 사용하기 위해 라이브러리를 사용하는데, 요구사항에서 라이브러리를 사용하지 말라고 하였으므로, transitioncancel 이벤트를 사용하였다.

transitionend이벤트와 transitioncancel이벤트는 양방향으로 발생한다. 즉, transitionend 이벤트가 발생하면 transitioncancel 이벤트는 발생하지 않는다.

댓글 공유

2주간의 페어 프로그래밍 회고

1. 무지성 코딩을 하지 말자.

생각 없이 코드를 짜면 내가 이 코드를 다른사람에게 설명해야하는 순간이 올 때, 말문이 턱 ! 막혀버리게 된다.

“여기서 왜 이 메서드를 사용했나요?”

“왜 이 변수는 이곳에 위치해야하나요?”

“이 함수의 이름으로 봐선 이러이러한 함수같은데 그게 맞나요?”

  • 위와 같은 질문들을 받았을 때, 나의 기준과 코드를 작성하는 신념을 세워 코드를 짜야지만 앞으로도 계속해서 발전하는 코드를 작성할 수 있다.

무지성 코딩을 하지 않기 위한 팁

  1. 상대방에게 말로 설명하면서 코딩을 하자

상대방에게 말로 설명하면서 코딩을 하다보면 논리에 맞지 않는 코드를 짤 수 없게 된다.

상대방이 이해하지 못하면 이해시킨 뒤 코딩을 이어간다.

2. 틀에 잡힌 생각을 버려라

처음부터 MVC 패턴을 머릿속에 담아두고 그 틀대로 코드를 짜려고 하지 말자. 틀을 가지고 코드를 짜게되면 하나의 패턴에 국한되어 생각하는 힘을 기르는 것을 막는다.

수업 때 배운 state와 setState를 사용한 MVC 패턴은 분명 좋은 패턴이지만, 모든 기능 구현에 필요하진 않다. 그러므로 처음부터 틀에 밖힌 사고를 하지 말고 차근차근 짜보면서 필요한 것들이 생긴다면 추가하도록 한다.

3. 성능과 가독성의 딜레마

성능을 생각하면 코드의 가독성이 떨어지고,

코드의 가독성을 생각하면 성능이 떨어지는 경우가 다반사이다.

성능이 미비한 차이이니 무조건 가독성만 챙기는 것은 옳지 않다. 각각의 상황에 맞게 성능과 가독성 중 하나의 우선순위를 택했다면, 선택받지 못한 것을 사용하였을 때, 장점도 알아두고 넘어가자.

4. 되는 이유와 되지 않는 이유

코드를 작성하다보면 막히는 부분이 분명있을 것이다. 그럴 때 마다 우선 어떤 부분에서 문제가 발생하는지를 파악하는 것이 우선이다.

그리고 그 문제를 해결하기 위해 구글링을 하거나 시도를 해보는데, 그 때마다 코드가 작동해서 좋아할 것이 아니고, 왜 코드가 작동하고, 왜 코드가 작동하지 않는지를 명확히 파악하고 진행해야 한다.

소감

처음 페어 프로그래밍을 시작했을 때는 코드에 대한 두려움은 없었다. 일단 요구사항에서 언급한 기능을 구현하기 위해 코드를 작성해 나갔는데, 그렇게 의식의 흐름대로 따라가다 보니 어느 순간 막히는 부분이 분명 생겼다. 그 때 마다 발생한 문제를 직면하고 파악하려 하지 않고 어떻게 하면 작동하게 될지에 집중하였다. 하지만 이렇게 작동에만 집중하다보니 내가 무엇을 모르고 무엇을 아는 것인지 메타인지가 떨어지게 되어 학습에 별로 도움이 되지 않았다.

내가 어떤 기능을 구현하기 위한 방법을 모르는 것과 문제가 발생했을 때, 문제를 직면하지 않고 구현에만 집착하는 것은 완전히 별개의 문제이다.

전자는 처음에는 당연히 모를 수 밖에 없는 부분으로 개발을 해나가면서 구글링을 통해 지식을 습득해나가면 되는 부분이지만, 후자는 다음에 똑같은 문제가 발생했을 때 같은 실수를 반복하고 발전이 있는 코드를 작성할 수 없게 된다.

그러므로, 어떤 문제가 발생하면 왜 그런 문제가 발생했는지 근본적인 원인을 파악하고 현재 상황에서 해당 문제의 원인을 해결하기 위해 어떤 방법이 있는지를 찾아보고 시도해보면서 성공하게 되면 이것보다 더 좋은 방법은 없는지를 생각해보는 방향으로 코딩을 해야한다는 것을 느꼈다.

댓글 공유

2번째 리팩터링 하면서 느낀점

scrollGotoTop

  • 스크롤 깊이와 스로틀 딜레이와 같은 정해진 숫자는 상수이므로, 가독성을 높이기 위해 대문자로 네이밍해주었다.
    • 은닉해야할 정보가 없기 때문에 클로저로 만들어줄 필요가 없다.

counter

  • 리팩터링 전 코드에서는 생성자 함수로 만들어서 인스턴스를 생성하여 클로저를 구현하였다
    • 하지만 이 방식은 불필요한 인스턴스 생성을 한다는 생각이 들었다.
    • 리팩토링한 코드는 즉시 실행 함수로 감싸서 변수 할당 같이 한번만 실행해도 되는 것들은 한 번만 실행되도록 만들어주었고, 내부의 데이터에 유일하게 접근할 수 있는 해당 함수를 return 해주어 데이터를 은닉해주었다.
    • counter에 관련된(해당 기능에만 사용되는) 데이터들과 함수들을 한데 묶어줘 응집도를 높여주었다
1
2
3
4
5
6
7
8
//document.body.addEventListener('click', e => {
// if (!e.target.matches('button')) return;

// e.target.matches('.increase') ? counter.increase() : counter.decrease();
//});

document.querySelector(".increase").addEventListener("click", counter.increase);
document.querySelector(".decrease").addEventListener("click", counter.decrease);
  • 증가, 감소 버튼을 이벤트 위임으로 작성해보았다. 이럴 경우 이벤트 핸들러 등록 갯수가 줄어들어 증감 버튼에 대한 로직을 한곳에서 관리할 수 있고 각각의 이벤트 마다 이벤트 핸들러를 등록하지 않아도 된다는 장점이 있다고 생각했다.
    • 하지만, 이벤트 위임의 경우 각 버튼이 다른 기능을 해야하는 경우라면 이벤트가 발생한 요소에 대한 조건문이 많아지고 그에 따라 코드가 복잡해질 수 있으므로 이벤트 위임 대신 각 요소에 이벤트 핸들러를 등록하는 방법으로 선택하였다.

isPalindrome

  • 문자열을 배열로 만들어 주는 경우 str.split('') 를 사용하는 것보단 문자열이 이터러블 요소인 것을 감안하여 스프레드 문법([…string])을 사용하였다.
    • 메서드를 사용하기위해 알아야 할 것들이 많은데 스프레드 문법은 알아야할 정보가 적어 코드의 이해성을 높일 수 있다.
1
2
3
4
5
//$result.textContent = isPalindrome(value) ? `"${value}" is a palindrome` : `"${value}" is not a palindrome`

$result.textContent = `"${value}" is ${
isPalindrome(value) ? "" : "not"
} a palindrome`;
  • 중복이 아닌 부분만 삼항 연산자를 이용하여 중복을 줄였다.
  • innerHTML 대신 textContent를 사용한 이유는 해당 요소에 할당한 값이 text 요소로만 이루어져 있기 때문이다.
    • innerText는 숨겨진 텍스트는 무시하고 사용자에게 보여지는 text만 가져올 수 있기 때문에 원하지 않는 오류가 발생할 수 있다. 내부에 특별히 스타일 적용이 없는 문자열을 할당할 때는 성능상 적합하지 않다.

DarkMode

1
2
3
4
5
// +getComputedStyle(document.querySelector(':root')).getPropertyValue('--transition-duration')

+getComputedStyle(document.documentElement).getPropertyValue(
"--transition-duration"
);
  • :root 에 존재하는 사용자 정의 변수를 가지고 올 경우에는 document.documentElement로 접근해야 한다.
    • document.querySelector(':root') 도 가능하지만 querySelector 메서드를 사용하는 것보다 프로퍼티를 접근하는 것이 더 효율적이므로 판단하여 사용하자.
1
const getCurrentMode = () => JSON.parse(localStorage.getItem("isDark"));
  • 로컬스토리지에 정보를 저장할 때 문자열로 저장되기 때문에 이를 getter 함수로 가져올 때 문자열 ‘true’ 값을 boolean 값으로 받기 위해 JSON.parse() 메서드를 사용하였다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// const setCurrentMode = selectedMode => {
// localStorage.setItem('isDark', selectedMode);
// toggleMode();
// };

init() {
// localStorage.getItem('isDark') === null ? setCurrentMode(userTheme) : toggleMode();
localStorage.setItem('isDark', localStorage.getItem('isDark') ?? userTheme);
toggleMode();
},
toggle() {
localStorage.setItem('isDark', !getCurrentMode());
toggleMode();
}
  • 기존에는 삼항연산자를 사용하였지만, 초기값이 없는 null 상태일 때 null 병합 연산자를 사용하여 초기값을 할당함으로써 함수를 줄일 수 있었다. 또한, setCurrentMode 함수가 필요없으므로 함수를 제거하여 코드의 이해성과 가독성을 높였다.
1
2
3
window.addEventListener("DOMContentLoaded", setDarkMode.init);

window.addEventListener("load", setDarkMode.preventBlink);
  • 렌더링 되는 시점이 다르면 다른 역할을 하는 함수로 판단한다.

popupModal

  • 변수를 사용한 이유는 단지 코드의 중복때문만이 아니라 이벤트가 발생할 때마다 호출될 필요가 없는 함수의 반환값을 변수에 저장하여 사용하기 위함이다.
  • 특정 목적을 위해서 밀접하게 연관된 기능들을 한데 모아 관리하면 해당 함수에 대한 이해도가 증가하게 되고 이는 유지보수성을 높이는데 도움이 된다. 또한 코드의 재사용성도 증가하는 장점도 있어 응집도를 높이는 구조로 코드를 설계하였습니다. 응집도를 높인 코드를 짜면서 정보를 안전하게 다룰 수 있도록 클로저를 구현하였습니다.
  • 요구사항에 맞게 Enter키 입력과 OK버튼 클릭시 중복을 줄이는 코드를 작성하기 위해서 이들을 form 태그로 감싸주었고 ‘submit’ 이벤트 타입으로 이벤트 핸들러를 등록하였습니다. cancel 버튼도 연관성을 위해 form 태그 안에 추가하였는데, cancel 버튼은 기본타입을 버튼으로 변경하여 submit 이벤트가 발생하지 않도록 하였습니다.
  • is-open 클래스를 이용하여 모달창을 보여주고 사라지도록 구현하였습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
const popupModal = (() => {
const $modalContainer = document.querySelector(".modal-container");
const $modal = document.querySelector(".modal");
const $popupMessage = document.querySelector(".popup-message");

return {
toggle() {
$modal.classList.toggle("is-open");
$modalContainer.classList.toggle("background-gray");
},
// remove() {
// $modal.classList.remove('is-open');
// $modalContainer.classList.remove('background-gray');
// },
render(e) {
e.preventDefault();
$popupMessage.textContent = `from popup : ${e.target.firstElementChild.value}`;
popupModal.toggle();
},
};
})();

document
.querySelector(".popup-form")
.addEventListener("submit", popupModal.render);
document.querySelector(".modal").addEventListener("click", (e) => {
if (!e.target.matches(".is-close")) return;

// popupModal.remove()
popupModal.toggle();
});

document.querySelector(".modal-container").addEventListener("click", (e) => {
if (!e.target.matches(".modal-container")) return;

// popupModal.remove()
popupModal.toggle();
});
  • e ⇒ {popupModal.render(e)} === popupModal 인수 전달안해줘도 event 객체 첫번째 인수로 넘어간다. 축약형
  • toggle 메서드를 사용하여 remove 메서드가 필요없어지므로 제거하였다.

stopWatch

  • 동일한 버튼을 구분할 때 textContent가 Start인지 Stop인지로 판단하는 것은 매우 좋지 않은 방식이므로, isStarted라는 현재 상태(시작/정지)를 판단하는 변수를 사용하였습니다.
  • leftButton / rightButton , laps/ title은 동일한 클래스를 가져 querySelectAll로 불러와서 인덱스 값으로 접근하는 것이 아닌 배열 디스트럭처링 할당으로 식별자를 할당해주었습니다.
    • 리팩터링 이후 이벤트 핸들러를 위한 leftButton, rightButton만 querySelectorAll로 받아오고 lap,title은 내부적으로 해결하였다.
  • 스톱워치내에서 관리해야할 데이터(isStarted, elapsedTime, timerId)를 하나의 객체에 담았다.
    • isStarted : 스톱워치가 시작되었는지 여부 (true / false)
    • elapsedTime : Start 버튼을 누르고 경과된 시간
      (setInterval의 delay마다 값이 증가하며, 이를 convertTime 함수를 통해 display에 표시한다.)
    • timerId : setInterval 함수의 리턴값을 가지며 clearInterval 함수와 함께 stop 기능을 구현하였다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const convertTime = (time) => {
const minutes =
Math.floor(time / 6000) < 10
? `0${Math.floor(time / 6000)}`
: `${Math.floor(time / 6000)}`;
const seconds =
Math.floor((time % 6000) / 100) < 10
? `0${Math.floor((time % 6000) / 100)}`
: `${Math.floor((time % 6000) / 100)}`;
const ms =
Math.floor(time % 6000) % 100 < 10
? `0${Math.floor(time % 6000) % 100}`
: `${Math.floor(time % 6000) % 100}`;

return `${minutes}:${seconds}:${ms}`;
};
  • convert 함수는 display에 보여질 시간으로 변환하고 그 값을 문자열로 리턴한다.
1
2
// $laps.children.length
$laps.childElementCount;
  • 리팩터링 전에는 자식노드의 갯수를 알기 위해서 자식노드(children)로 접근한 다음 길이(length)로 접근하여 값을 얻었지만, childElementCount라는 프로퍼티를 이용하여 한 번에 값을 얻을 수 있었다.
1
$laps.insertAdjacentHTML("beforeend", newLap);
  • 리팩터링 전에는 laps를 배열로 관리하여 이를 innerHTML으로 하나가 추가되더라도 모든 부분을 렌더링하였지만, 리팩터링 후에는 insertAdjacentHTML 메서드를 사용하여 HTML을 동적으로 추가해주어 추가된 부분만 렌더링 되도록 해주었다. (참고 : 여기서 beforeend 는 해당 요소의 자식노드 마지막에 삽입하는 것을 의미한다)
  • reset 함수에서도 insertAdjacentHTML처럼 추가된 부분만 삭제해주고 싶었지만, 고려해야할 조건들이 많아져 가독성을 위해 기존의 innerHTML을 사용하였다.
1
2
3
4
5
6
7
8
return {
toggle,
lapAndReset() {
!data.isStarted ? addLap() : reset();
},
};

$rightButton.addEventListener("click", stopWatch.lapAndReset);
  • 이벤트 핸들러에서 data.isStarted를 이용하여 조건에 따라 함수를 실행해주려 하였으나 클로저로 구현하였기 때문에 클로저 바깥에서 data.isStarted 값을 참조할 수 없기 때문에 클로저 내부에서 data.isStarted를 참조하여 조건에 맞게 해당 함수를 실행하는 함수를 return하였다.

댓글 공유

Form Validation 구현 중 느낀점

1. 모든 코드를 setState로 틀을 고정하여 생각하지 말자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
let state = {
id: "pending",
pw: "pending",
};

const [$idSuccessIcon, $pwSuccessIcon] = [
...document.querySelectorAll(".icon-success"),
];
const [$idErrorIcon, $pwErrorIcon] = [
...document.querySelectorAll(".icon-error"),
];
const [$idErrorMessage, $pwErrorMessage] = [
...document.querySelectorAll(".error"),
];
const $signinButton = document.querySelector(".button");

const render = () => {
$idSuccessIcon.classList.toggle("hidden", state.id !== "success");
$idErrorIcon.classList.toggle("hidden", state.id !== "error");

$pwSuccessIcon.classList.toggle("hidden", state.pw !== "success");
$pwErrorIcon.classList.toggle("hidden", state.pw !== "error");

$idErrorMessage.textContent =
state.id === "error" ? "이메일 형식에 맞게 입력해 주세요." : "";
$pwErrorMessage.textContent =
state.pw === "error" ? "영문 또는 숫자를 6~12자 입력하세요." : "";

if (state.id === "success" && state.pw === "success")
$signinButton.removeAttribute("disabled");
else $signinButton.setAttribute("disabled", "");
};

const setState = (newState) => {
state = { ...state, ...newState };
render();
};
const checkIdValidation = (input) => {
if (input === "") {
setState({ id: "pending" });
return;
}
const regExp =
/^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*\.[a-zA-Z]{2,3}$/;

setState({ id: regExp.test(input) ? "success" : "error" });
};

const $signIdInput = document.querySelector("#signin-userid");

$signIdInput.addEventListener(
"keyup",
_.debounce(() => {
checkIdValidation($signIdInput.value);
}, 500)
);
  • 위 코드는 만약 인풋에 입력해야할 값이 늘어 유효성 검사 항목이 늘어나게 된다면 유지보수가 어려워진다. 또한, DOM API에 의존적인 코드이므로 HTML 코드가 변경될 시 유지보수가 어려워질 수 있다.

  • 위 코드는 처음부터 구조를 setState로 잡고 데이터를 변경하는 함수는 setState가 도맡아 하고 render는 화면에 보여주는 역할을 모은 함수 이런 식으로 코드를 작성하였다.

하지만 위와 같이 틀을 미리 잡고 코드를 작성하는 것은 별로 좋지 않는 습관이다.

왜냐하면, state를 사용해야하는 상황이 아님에도 setState를 사용하여 그 구조에 억지로 끼워맞추게 되고 그렇게 되면 코드의 가독성과 이해도가 떨어질 수 있고 발전이 없는 코드를 작성하게 된다.

그러므로 코드를 작성할 때는, 우선 구현이 되도록 작성을 하되, 생각을 하면서 그 때 그 때 상황에 맞게 코드를 개선해나가야 한다.

위 코드를 리팩터링한 결과는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
const signinSchema = {
userid: {
value: "",
get valid() {
return /^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*\.[a-zA-Z]{2,3}$/.test(
this.value
);
},
message: "이메일 형식에 맞게 입력해 주세요.",
},
password: {
value: "",
get valid() {
return /\w{6,12}$/.test(this.value);
},
message: "영문 또는 숫자를 6~12자 입력하세요.",
},
get valid() {
return this.userid.valid && this.password.valid;
},
};

const checkValidation = (target) => {
signinSchema[target.name].value = target.value;

if (signinSchema[target.name].value === "") {
document
.querySelector(`#${target.id} ~ .icon-success`)
.classList.toggle("hidden", true);
document
.querySelector(`#${target.id} ~ .icon-error`)
.classList.toggle("hidden", true);
document.querySelector(`#${target.id} ~ .error`).textContent = "";
return;
}

document
.querySelector(`#${target.id} ~ .icon-success`)
.classList.toggle("hidden", !signinSchema[target.name].valid);
document
.querySelector(`#${target.id} ~ .icon-error`)
.classList.toggle("hidden", signinSchema[target.name].valid);
document.querySelector(`#${target.id} ~ .error`).textContent = !signinSchema[
target.name
].valid
? signinSchema[target.name].message
: "";
};

document.querySelector(".signin").addEventListener(
"input",
_.debounce((e) => {
checkValidation(e.target);
}, 500)
);
  • 리팩터링한 코드는 우선 setState로 틀을 구성하지 않았다. 굳이 처음부터 setState 함수로 데이터를 변경하고 렌더링까지 해주는 구조를 가질 필요가 없다.

마치 setState로 정해놓고 코드를 작성하는 것은 아이가 어른 흉내를 내는 듯한 느낌이 든다.

  • email과 password의 input에 관련이 있는 데이터를 객체로 관리하고 있다.

  • input에 이벤트가 발생할 때 마다 input의 value 값을 객체의 데이터에 변경해줘야한다.

  • 접근자 프로퍼티를 사용하여 접근자 프로퍼티를 참조하면 input.value 값과 정규 표현식이 같은지 확인하여 유효성 검사를 결과를 반환한다. 접근자 프로퍼티를 사용한 이유는 해당 접근자 프로퍼티를 참조할 때마다 갱신된 해당 프로퍼티의 유효성 검사 결과를 얻기 위해서 get을 붙여 접근자 프로퍼티로 만들었다.

각 객체에 필요한 데이터를 한곳에서 관리하고 이들을 이용하여 요구사항을 어떻게 충족시킬지 생각해보자.

2. DOM API 사용을 줄이자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const checkValidation = (target) => {
signinSchema[target.name].value = target.value;

if (signinSchema[target.name].value === "") {
document
.querySelector(`#${target.id} ~ .icon-success`)
.classList.toggle("hidden", true);
document
.querySelector(`#${target.id} ~ .icon-error`)
.classList.toggle("hidden", true);
document.querySelector(`#${target.id} ~ .error`).textContent = "";
return;
}

document
.querySelector(`#${target.id} ~ .icon-success`)
.classList.toggle("hidden", !signinSchema[target.name].valid);
document
.querySelector(`#${target.id} ~ .icon-error`)
.classList.toggle("hidden", signinSchema[target.name].valid);
document.querySelector(`#${target.id} ~ .error`).textContent = !signinSchema[
target.name
].valid
? signinSchema[target.name].message
: "";
};
  • 최대한 DOM API를 적게 사용하기 위해 해당 이벤트 핸들러의 이벤트 객체를 통해서 다른 요소에 접근할 수 있도록 코드를 작성하였다.

  • document.querySelector(`#${target.id} ~ .icon-success`) 의 의미는 이벤트가 발생한 요소의 id값을 선택자로 가져와서 해당 선택자의 일반 형제 요소 중 클래스가 icon-success인 요소를 선택하라는 뜻이다.

DOM API로 요소를 선택할 때 마다 시간이 오래 걸리고 어려운 작업이므로, 이러한 과정을 줄이도록 생각하며 코드를 작성한다.

3. input 이벤트 타입

1
2
3
4
5
6
document.querySelector(".signin").addEventListener(
"input",
_.debounce((e) => {
checkValidation(e.target);
}, 500)
);
  • 리팩터링 이전 코드에서는 keyup 이벤트를 사용하여 email과 password의 input에서 발생하는 이벤트를 이벤트 핸들러로 각각 등록해주었다.

  • 리팩터링한 코드에서는 이벤트 위임을 통해서 이벤트를 다뤄주었다.

  • 또한, 인수로 이벤트가 발생한 객체를 전달해주고 이벤트 핸들러 함수 내부에서는 target 매개변수를 해당 인수를 받아 배치해주었다.

  • e.target.name은 HTML 문서에서 name 어트리뷰트를 가리킨다. 이러한 점을 알게되어 HTML이 매우 중요하다는 것을 깨달았다.

댓글 공유

Toaster 리팩터링 중 느낀점

1. 함수의 인수는 3개를 넘지 말자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const generateToast = (messageType) => {
const [type, title, message] = messageType;
const $newToast = document.createElement("div");
$newToast.style.bottom = "0";
$newToast.classList.add("toast", type);

$newToast.innerHTML = `
<h4 class="toast-title">${title} ${
[...document.querySelectorAll("body .toast")].length
}</h4>
<div class="toast-message">
<svg width="24" height="24">
<use xlink:href="#${type}" />
</svg>
<p>${message}</p>
</div>
<a class="toast-close">&times;</a>`;

return $newToast;
};
  • 배열 스트럭처링으로 매개변수를 함수 내부에서 나눠서 배치하였다.

2. 함수를 사용하는 이유를 고민하자.

1
2
3
4
5
6
7
8
9
10
11
12
const addToast = (messageType) => {
const $newToast = generateToast(messageType);

$body.appendChild($newToast);
lineUpToast();

setTimeout(() => {
$newToast.remove();

lineUpToast();
}, 3000);
};
  • setTimeout 함수를 따로 함수로 빼서 사용해보면 어떨까 생각해보았지만, 만약 함수로 들어가게 된다면 오히려 그 함수를 이해하기 위해 더 큰 노력을 쏟아야 할 수 있으므로 가치 판단을 하여 함수로 넣을지 말지를 고민하자.

Autocomplete 리팩터링 중 느낀점

1. setState 함수는 함수 내부의 데이터를 변경만 하는 함수

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let state = {
countryCode: [],
};

const setState = (newState) => {
state = { ...state, ...newState };
render();
};

const init = () => {
setState({ countryCode });
};

document
.querySelector(".autocomplete-toggle-button")
.addEventListener("click", () => {
$suggester.classList.toggle("hide");
$searchInput.focus();

autocomplete.init();
});
  • 이벤트 핸들러는 모델에서 return 해준 함수만 사용할 수 있도록 하여 관심사를 분리해준다.

  • 또한, 이벤트 핸들러가 setState 함수를 사용하여 데이터를 직접 변경하는 것은 옳지 않다. 위와 같이 init 함수를 통해서 사용하는 것으로 한다. 왜냐하면 setState 함수는 데이터 변경에만 관여하는 함수이지 다른 기능을 하는 함수가 아니기 때문이다.

2. 함수 역할의 분리

1
2
3
4
5
6
7
8
9
10
11
12
const findCountry = (inputValue) => {
const regExp = new RegExp(inputValue, "i");

const newCountryCode = countryCode
.filter(([, country]) => country.match(regExp))
.map(([code, country]) => [
code,
country.replace(regExp, `<strong>${country.match(regExp)}</strong>`),
]);

setState({ countryCode: newCountryCode });
};
  • 이전 코드에서는 findCountry 함수가 하는 역할을 setState 함수내에 있어 이를 역할에 맞게 구분해주었다.

  • 또한, 배열 디스트럭처링을 사용하여 가독성을 높였다.

([, country]) 처럼 매개변수로 배열 디스트럭처링 사용할 때, 앞의 매개변수를 사용하지 않는다면, 빈칸으로 두어도 된다.

3. 함수의 가독성을 높이자.

1
2
3
4
const selectCountry = (target) => {
$toggleButtonSpan.innerHTML = target.firstElementChild.innerHTML;
$toggleButtonSpan.classList.add("country");
};
  • selectCountry 함수의 매개변수가 이벤트가 발생한 요소를 전달해주도록 코드를 작성하여 더욱 직관적으로 작성했다.

Carousel 리팩터링 중 느낀점

1. state로 관리해야하는 데이터와 그렇지 않은 데이터

1
2
3
4
5
6
7
let state = {
currentSlide: 1,
};

let isTransitioned = true;
const DURATION = 500;
const newImages = [images[images.length - 1], ...images, images[0]];
  • 이번 과제를 하면서 어떤 데이터를 state로 관리해야할지 결정하는 것이 힘들었다. 짝 코딩을 하면서 나름 이유와 컨벤션을 정하였다.

state로 관리해야하는 데이터

  1. 사용자의 액션에 의해서 변경되는 데이터

  2. 실제 화면의 렌더링에 영향을 끼치는 데이터

  3. 서버에 저장해야할 필요가 있는 데이터

위 3가지 항목에 해당된다면 해당 데이터를 state로 관리하는 것이 옳다고 판단하였다.

그에 대한 근거는 다음 예시를 통해 알아보자.

1
2
3
4
5
6
7
const completeTransition = () => {
setState({ isTransitioned: true });
};

const completeTransition = () => {
isTransitioned = true;
};
  • 모델안의 completeTransition 함수는 state 데이터를 수정하고 렌더링을 해주는 코드였다. 하지만 위 데이터는 화면에 보여지는 데이터도 아니고, 사용자의 액션에 의해 직접적으로 변경되었다기 보다는 사용자가 액션을 발생 시켰을 때 개발자가 눈속임으로 화면을 보여주기 위함이기 때문이다.

즉, completeTransition 데이터를 사용자에게 직접적으로 보여주지도 않고 서버에 전송해야할 데이터도 아니고 실제 화면에 렌더링에 영향을 끼치는 데이터가 아니기 때문에 state로 관리 하지 않았다.

2. Model과 Controller 패턴의 사용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const carousel = () => {
const carouselSlider = (() => {

...

return {
init,
setStyle,
prev,
next,
completeTransition,
}
})();

window.addEventListener('DOMContentLoaded', carouselSlider.init);
}

carousel(document.querySelector('.carousel'), [images...])
  • addEventListener가 사용할 수 있는 데이터는 Model에서 반환해준 함수만 사용할 수 있다. 이러한 구조를 만듦으로서 사용자는 자신이 어떤 함수를 사용해야하는지 명시적으로 알 수 있고 다른 함수에 대한 고민을 하지 않아 코드의 이해도를 높일 수 있다.

3. DOMContentLoaded와 load 이벤트 핸들러

1
2
3
window.addEventListener("DOMContentLoaded", carouselSlider.init);

window.addEventListener("load", carouselSlider.setStyle);
  • 이미지가 로드된 다음에 이미지의 width 값과 속성을을 설정해줘야지만 제대로 동작하므로 위와 같이 로드되는 시점에 따라 코드를 구분하였다.

  • 또한 요구사항에서 이미지가 로드되어 carousel의 width가 정해진 후 opacity를 1로 바꿔줘야 하므로 로드 시점에 따라 이벤트 핸들러를 구분하는 것이 옳다.

댓글 공유

darkMode 리팩터링 중 느낀점

1. setDarkMode 함수의 응집도를 높이고 관심사를 구분하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const setDarkMode = (() => {
const $body = document.body;

const userTheme = window.matchMedia("(prefers-color-scheme: dark)").matches;
const transitionDuration =
+getComputedStyle(document.documentElement).getPropertyValue(
"--transition-duration"
) * 1000;

const getCurrentMode = () => JSON.parse(localStorage.getItem("isDark"));

const toggleMode = () => {
$body.classList.toggle("dark", getCurrentMode());
};

const setCurrentMode = (selectedMode) => {
localStorage.setItem("isDark", selectedMode);
toggleMode();
};
return {
toggle() {
setCurrentMode(!getCurrentMode());
},
init() {
localStorage.getItem("isDark") === null
? setCurrentMode(userTheme)
: toggleMode();
},
preventBlink() {
setTimeout(() => {
$body.classList.remove("hide");
}, transitionDuration);
},
};
})();

document
.querySelector(".toggle-button")
.addEventListener("click", setDarkMode.toggle);

window.addEventListener("DOMContentLoaded", setDarkMode.init);

window.addEventListener("load", setDarkMode.preventBlink);
  • setDarkMode 함수와 관련된 변수와 함수를 한곳에 모아 응집도를 높이고 즉시실행함수로 감싸고 클로저를 구현하였다.

  • 즉시실행함수의 반환값인 toggle, init, preventBlink 함수는 자유변수를 참조하고 외부함수보다 생명주기가 긴 클로저이다.

  • preventBlink 함수와 init 함수는 결국 렌더링과 관련된 함수여서 하나로 합칠까 생각했지만, 그 경우, 이미지 파일이 많은 프로그램인 경우 이미지가 로드되기 전에 darkMode를 동작이 제대로 되지 않을 수 있다고 판단하여 다른 함수로 구분하였다.

  • addEventListener는 가능한 한줄로 쓰자는 팀원과의 컨벤션을 지키기 위해 위와같이 작성하였고 이벤트 핸들러가 많아지게 되면서 복잡해지더라도 한줄로 있으면 가독성을 높일 수 있을 것이라 생각했다.

popupModal 리팩터링 중 느낀점

1. 변수를 사용한 이유

1
2
3
const $modalContainer = document.querySelector(".modal-container");
const $modal = document.querySelector(".modal");
const $popupMessage = document.querySelector(".popup-message");
  • 위의 document.querySelector() DOM API는 처음에 한번만 요소를 가져와 변수에 할당해주면 된다. 굳이 이벤트 핸들러 함수 내부에 적어서 함수가 호출될 때마다 DOM API까지 같이 호출될 필요가 없으므로 변수에 값을 저장해서 사용하였다.

2. 누군가가 코드를 읽을 때, 한번 더 생각하게 하지 않도록 작성하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 1번
toggle(method) {
$modal.classList[method]('is-open');
$modalContainer.classList[method]('background-gray');
},

// 2번
toggle() {
$modal.classList.toggle('is-open');
$modalContainer.classList.toggle('background-gray');
},
remove() {
$modal.classList.remove('is-open');
$modalContainer.classList.remove('background-gray');
},
  • 처음에 중복을 줄이기 위해 1번으로 구현했었는데, 오히려 직접 사용해보니 2번의 경우가 가독성이 더 낫다고 판단했다. 그 이유는 이벤트 핸들러에서 toggle(‘remove’) 이런식으로 사용하는 것보다 toggle, remove를 사용하는 것이 더 깔끔하기 때문이다.

3. 이벤트 핸들러 축약표현 알아두기

1
2
3
4
5
6
7
render(e) {
e.preventDefault();
$popupMessage.textContent = `from popup : ${e.target.firstElementChild.value}`;
popupModal.toggle('remove');
}

document.querySelector('.popup-form').addEventListener('submit', render);
  • render 함수를 이벤트 핸들러로 등록할 때, event 객체를 받아서 e.preventDefault() 메서드를 실행하는데, 이벤트 핸들러에서는 인수를 전달해주지 않아도 축약표현으로 인식하여 event 객체를 해당 위치에 전달해준다.

stopWatch 리팩터링 중 느낀점

1. 배열 디스트럭처링 사용

1
const [$leftButton, $rightButton] = [...document.querySelectorAll(".control")];
  • leftButton, rightButton / laps, title은 동일한 클래스를 가져 querySelectAll로 불러와서 인덱스 값으로 접근하는 것이 아닌 배열 디스트럭처링 할당으로 식별자를 할당해주었습니다.

2. 버튼 요소의 textContent 값으로 조건문을 구분하는 것은 좋지 않다.

1
2
3
4
5
6
7
8
9
10
11
12
// 1번
if ($leftButton.textContent === "Stop") {
...
}

// 2번
let state = {
isStarted:true
}
if (state.isStarted) {
...
}
  • 1번 처럼 textContent로 조건문을 사용하게되면 나중에 textContent를 수정하게되면 유지보수가 어려워지므로 상태로서 관리하는 것을 추천한다.

3. 함수는 각자의 역할에 맞는 코드를 포함하도록 작성해야한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
const render = () => {
if (state.isStarted) {
$leftButton.textContent = "Stop";
$rightButton.textContent = "Lap";
$rightButton.removeAttribute("disabled");

$laps.innerHTML = `Laps
${state.laps.map((_, index) => `<div>${index + 1}</div>`).join("")}
`;
$time.innerHTML = `Time
${state.laps.map((lap) => `<div>${lap}</div>`).join("")}
`;
} else {
$leftButton.textContent = "Start";
$rightButton.textContent = "Reset";
}
};

const setState = (newState) => {
state = { ...state, ...newState };

render();
};

const toggleStarted = () => {
setState({ isStarted: !state.isStarted });

updateTime();
};

const addLap = () => {
const newLap = $display.textContent;

setState({ laps: [...state.laps, newLap] });
};
  • render 함수는 leftButton, rightButton을 클릭했을 때, 화면에 글자가 바꾸는 일을 해준다.

  • setState 함수는 데이터를 변경하고 그 데이터를 가지고 화면에 render 하는 함수를 호출하는 역할을 한다.

  • toggleStarted 함수는 isStarted 데이터를 변경시키고 시간을 갱신하는 역할을 한다.

  • addLap 함수는 $display의 textContent값으로 laps 데이터를 변경시키는 역할을 한다.

스톱워치에 아직 리팩터링 중인데, 버튼은 2개인데 가정해야할 상황이 4가지여서 어떻게 짜면 좋을지 고민중이다…

댓글 공유

CarouselSlider 구현중 느낀점

1. 구현 중 필요한 상수 또는 변수는 따로 구분해주자.

1
2
3
4
5
6
7
8
9
10
// Data
const duration = 500;
const direction = {
prev: -1,
next: 1,
};
const newImages = [images[images.length - 1], ...images, images[0]];

let currentSlide = 1;
let transitionComplete = true;
  • 로직 중간에 재할당이 발생하지 않은 요소는 const 키워드로 선언하여 의도치 않은 재할당을 방지한다.

  • currentSlide는 DOM API를 사용하여 필요할 때마다 가져왔는데, 값을 데이터 부분에서 한번만 가져와서 그 값을 가지고 사용하는 방법으로 하면 DOM API를 자주 사용하여 발생하는 코드의 복잡성을 해결할 수 있다.

2. transitionend 이벤트와 transitioncancel 이벤트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const setTransitionCompleteTrue = () => {
transitionComplete = true;
};

$container.addEventListener("transitionend", setTransitionCompleteTrue);
$container.addEventListener("transitioncancel", setTransitionCompleteTrue);

$container.addEventListener("click", (e) => {
if (!e.target.matches(".carousel-control") || !transitionComplete) return;

e.target.matches('.prev') // prettier-ignore
? slideImage(direction.prev, 0, newImages.length - 2)
: slideImage(direction.next, newImages.length - 1, 1);
});
  • transitionend 이벤트는 요소 노드의 trnasition 이벤트가 완료될 때, 이벤트 핸들러가 호출된다.

  • transitioncancel 이벤트는 요소 노드의 trnasition 이벤트가 취소될 때, 이벤트 핸들러가 호출된다.

처음에 캐러셀을 구현할 때, 1-4번 사진에서 next 버튼 클릭시 transition-delay가 유지되면서 다음 사진으로 이동했는데, 연타로 클릭하니 transition-delay가 유지되지 않고 다음 사진으로 이동하여 이를 어떻게 구현할 지 생각해보고 구글링을 하다가 transition 이벤트를 알게되어 사용하였다.

transition이 완료될 때, transitionComplete를 true로 할당하고 클릭 이벤트가 발생하였을 때는 위와 같이 이미지를 슬라이드 해주는 함수를 구현하였다.

slideImage 함수 내부의 로직은 위 부분에서 알 필요가 없고 위 부분에서는 단지 클릭 이벤트가 일어났을 때, 이미지를 슬라이드 해주는 관심사가 중요하기 때문에 다른 것은 알 필요가 없도록 코드를 구현하는 것이 바로 관심사의 구분이다.

transitionend 이벤트가 가끔 사라지는 경우

transitioncancel

next 버튼, prev 버튼을 연타해서 클릭하거나 예상치 못한 경우에 transition이벤트가 완료되지 않고 갑자기 사라지는 경우가 발생하였다.

위와 같이 갑자기 클릭을 계속하여도 transitionend 이벤트가 동작하지 않아 transitionComplete값이 true로 할당되지 않으므로 정상동작하지 않는 것을 볼 수 있다.

1
$container.addEventListener("transitioncancel", setTransitionCompleteTrue);

이러한 예외경우를 위해서 transitioncancel이벤트로 이벤트 핸들러로 다뤄주면 예외 처리가 가능하다.

3. 마지막 캐러셀이 슬라이드 되고 난 후 다시 처음 슬라이드로 이동시키기 (눈속임)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const slideImage = (direction, from, to) => {
transitionComplete = false;

currentSlide += direction;

setSlidesProperty("--duration", duration);
setSlidesProperty("--currentSlide", currentSlide);

if (currentSlide === from) {
setTimeout(() => {
currentSlide = to;

setSlidesProperty("--duration", 0);
setSlidesProperty("--currentSlide", currentSlide);
}, duration);
}
};

코드를 해석하자면 다음과 같다.

  1. transitionend 이벤트가 발생하면 transitionComplete가 true로 할당이 된다. 즉, transition이벤트가 끝나면 currentSlide를 해당 방향으로 한칸 이동시킨다.

  2. 만약 현재 슬라이드가 마지막(from)이라면, transition-delay가 완료되는 시간인 duration 후에 currentSlide에 처음 슬라이드 번호를 할당하고 duration 속성을 0으로 초기화하여 transition 이벤트가 발생하지 않도록 한 후 현재 보이는 슬라이드를 처음 슬라이드 번호로 바뀐 값으로 설정해준다.

이렇게 되면 사용자는 마지막 슬라이드로 이동했을 때, 다시 처음 슬라이드로 돌아온 것으로 무한 루프를 구현한 것처럼 느끼게 된다.

Counter 리팩토링하면서 느낀점

1. 이벤트 핸들러 부분과 함수의 역할에 따라 구분해주자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 리팩토링 전
const counter = (function Counter() {
let counter = 0;

return function (helper) {
counter = helper(counter);
return counter;
};
})();

const increaseCount = (counter) => {
counter += 1;
return counter;
};
const decreaseCount = (counter) => {
counter = counter > 0 ? counter - 1 : 0;
return counter;
};

const $counter = document.querySelector(".counter");

document.querySelector(".increase").addEventListener("click", () => {
$counter.textContent = counter(increaseCount);
});

document.querySelector(".decrease").addEventListener("click", () => {
$counter.textContent = counter(decreaseCount);
});
  • 리팩토링 전에는 헬퍼함수라는 콜백함수를 받아 클로저를 구현해서 increaseCount, decreaseCount 함수가 counter 함수에서만 사용하는데 전역코드에 존재하므로 다른 곳에서도 사용할 수 있는 함수로 오해할 수 있으므로 이들 함수간의 응집도를 눂혀줘야한다.

  • 또한, 이벤트 핸들러 함수가 렌더링을 해주고 있는데 이보다 렌더링 해주는 함수, 카운트 증가시키는 함수, 카운트 감소시키는 함수 등으로 역할에 따라 구분하는 것이 적절하다.

위 두가지 개선사항을 반영하여 나온 코드가 아래 코드이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 리팩토링 후
const counter = (() => {
let count = 0;

const $counter = document.querySelector(".counter");

const render = () => {
$counter.textContent = count;
};

return {
increase() {
count += 1;
render();
},
decrease() {
if (count > 0) count -= 1;
render();
},
};
})();

document.querySelector(".increase").addEventListener("click", counter.increase);
document.querySelector(".decrease").addEventListener("click", counter.decrease);
  • counter 식별자에 즉시실행함수의 반환값(객체)를 할당하여 처음 Js 파일이 로드되었을 때, 한번만 호출되거나 실행되는 부분과 여러번 호출되는 부분을 구분하였다.

  • 이벤트 핸들러가 직관적으로 클릭하면 무슨일을 하는 지 알 수 있다.

더 나아가 위 코드를 각 역할에 맞도록 모듈로 구분할 수도 있다.

isPalindrome 리팩토링 중 느낀점

1. 응집도 높이기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 리팩토링 전
const $input = document.querySelector(".palindrome-input");

const isPalindrome = (string) => {
const newString = string.toUpperCase().replace(/[^A-Za-z0-9가-힣]/g, "");
if (newString === "") return false;

const reversedString = newString.split("").reverse().join("");
return newString === reversedString;
};

document
.querySelector(".palindrome-checker")
.addEventListener("submit", (e) => {
e.preventDefault();
const value = $input.value.trim();
if (value === "") return;

document.querySelector(".palindrome-result").textContent = isPalindrome(
value
)
? `"${value}" is a palindrome`
: `"${value}" is not a palindrome`;

$input.value = "";
});
  • isPalindrome함수는 아래 이벤트 핸들러함수 안에서만 사용될 것인데 전역 코드에 있으므로 응집도를 높혀줘야한다.

  • split을 사용하여 배열화 하는 것보다 스프레드 문법을 사용하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 리팩토링 후
const checkPalindrome = (() => {
const $input = document.querySelector(".palindrome-input");
const $result = document.querySelector(".palindrome-result");

const isPalindrome = (string) => {
const newString = string.toUpperCase().replace(/[^A-Za-z0-9가-힣]/g, "");
if (newString === "") return false;

const reversedString = [...newString].reverse().join("");
return newString === reversedString;
};

return (e) => {
e.preventDefault();

const value = $input.value.trim();
if (value === "") return;

$result.textContent = `"${value}" is ${
isPalindrome(value) ? "" : "not"
} a palindrome`;

$input.value = "";
};
})();

document
.querySelector(".palindrome-checker")
.addEventListener("submit", checkPalindrome);
  • isPalindrome 함수와 이벤트 핸들러 함수의 응집도를 높히기 위해 즉시실행함수로 감싸주었다.

  • 즉시실행함수는 이벤트 객체를 인수로 받는 함수를 반환한다. 이 함수에다가 리팩토링 전에 이벤트 핸들러 안에 있던 코드들을 담아주었다. 즉시실행함수가 반환하는 함수는 외부함수의 식별자를 참조하고 있고 외부함수보다 생명주기가 더 길어서 클로저이다.

  • 문자열을 렌더링해주는 부분에서 조건에 따라 ‘not’이 있고 없고 차이가 있으므로 바꿔주었다.

  • 이터러블 요소에는 스프레드 문법을 사용할 수 있다. 문자열은 이터러블하므로 split() 대신에 spread 문법을 사용하여 배열화 해주었다.

댓글 공유

carousel Tip

카테고리 Pair Programming, level 1

CarouselSlider 구현 중 느낀점

1. 이미지가 로드되는 시점과 동적 HTML 생성 시점이 다르다.

1
2
3
4
5
6
7
8
9
// DOM이 로드되고 동적으로 HTML 생성(렌더링)
window.addEventListener("DOMContentLoaded", render);

// 이미지 로드 후 width 및 opacity 할당
window.addEventListener("load", () => {
$container.style.width = `${document.querySelector("img").clientWidth}px`;
$container.style.opacity = 1;
$container.firstElementChild.style.setProperty("--duration", 400);
});
  • 동적으로 HTML을 렌더링해주는데 DOMContentLoaded가 될 때, 이미지의 width를 가져오고 싶었지만 동적 HTML이 생성되고 약간의 시간 후에 이미지가 로드되어 동적 HTML 생성될 때, 이미지태그의 너비를 가져올 수가 없었다.

  • --duration은 요구 사항에 맞게 초기 렌더링 때 설정해준다.

2. CSS inline style이 아닌 external CSS 값 가져오기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$container.addEventListener("click", (e) => {
// 1. prev || next return
// 2. if (prev) / if (next)
if (!e.target.matches(".carousel-control")) return;
e.target.matches(".prev")
? $container.firstElementChild.style.setProperty(
"--currentSlide",
+getComputedStyle($container.firstElementChild).getPropertyValue(
"--currentSlide"
) - 1
)
: e.target.matches(".next")
? $container.firstElementChild.style.setProperty(
"--currentSlide",
+getComputedStyle($container.firstElementChild).getPropertyValue(
"--currentSlide"
) + 1
)
: "";
});
  • document.querySelector(‘div’).style.getPropertyValue() 메서드는 해당 노드의 인라인 스타일의 속성값을 가져올 수 있다.

  • getComputedStyle(document.querySelector(‘div’)).getPropertyValue() 메서드는 해당 노드의 렌더링을 마친 CSS 속성값을 가져올 수 있다. 즉, 외부 CSS 파일의 속성값을 가져올 수 있다.

3. document.querySelector(‘img’) 요소가 가진 width 종류

  • cssWidth : 컨텐츠의 너비를 말한다.
  • clientWidth : padding을 포함한 너비를 말한다.
  • offsetWidth : border까지 포함한 너비를 말한다.

처음에 이미지 태그의 너비를 자바스크립트로 가져와야하는데, 이미지 태그가 가진 너비 요소가 여러가지라서 각 너비의 특징에 대해 알게 되었다.

앞으로 해당 요소 노드를 console 창에 찍어보면서 어떤 프로퍼티를 포함하고 있는지 확인해보면서 내게 필요한 프로퍼티가 무엇인지 찾아보는 팁을 얻었다.

댓글 공유

modal 창 구현 중 느낀점

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<body>
<div class="modal-container">
<div class="modal">
<h2>Hello</h2>
<p>희준이주화이팅</p>
<form action="/" class="popup-form">
<input type="text" class="popup-input" />
<button class="popup-ok">OK</button>
</form>
<button class="popup-cancel is-close">Cancel</button>
<button class="popup-close">
<i class="bx bx-x is-close"></i>
</button>
</div>
</div>
<script>
const $modal = document.querySelector(".modal");
const $modalContainer = document.querySelector(".modal-container");

document.querySelector(".popup-button").addEventListener("click", () => {
$modal.classList.toggle("is-open");
$modalContainer.classList.toggle("background-gray");
});
</script>
</body>
  • body 태그 안에 modal div 박스 하나만 만드는 것이 아닌 다음과 같이 modal container로 감싸주는 것이 편하다.

그렇지 않으면 modal 배경색을 입히기가 까다로워진다.

stopwatch 구현 중 느낀점

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let timeId;
const updateTime = () => {
const startTime = Date.now();

timeId = setInterval(() => {
const date = new Date(Date.now() - startTime);
const milliseconds = Math.floor(date.getMilliseconds() / 10);
const seconds = date.getSeconds();
const minutes = date.getMinutes();

$display.textContent = `${minutes < 10 ? `0${minutes}` : `${minutes}`}:${
seconds < 10 ? `0${seconds}` : `${seconds}`
}:${milliseconds < 10 ? `0${milliseconds}` : `${milliseconds}`}`;
}, 10);
};
  • 시간을 수정하는 로직을 함수로 구분하여 역할을 나눠보려 하였다.

Model, Controller, View 생각하면서 리팩토링 해보자.

Tab 구현 중 느낀점

1. 비동기 코드로 전달받은 데이터 다루기

1
2
3
4
5
6
const state = {
datas: [],
};
fetchTabsData().then((tabDatas) => {
state.datas = tabDatas;
});
  • fetchTabsData() 함수가 프로미스르 반환하는데, 프로미스 콜백함수가 1초 뒤에 데이터를 resolve 인자로 전달해준다. 그러므로 비동기로 받아온 데이터를 state로 관리해주도록 하였다.

2. CSS 사용자 정의 변수 변경 또는 값 취득하기

1
2
3
const $tabs = document.querySelector(".tabs");

$tabs.style.setProperty("--tabs-length", 3);
  • Js를 사용하여 CSS에서 설정한 사용자 정의 변수를 가져올 수도 있고 위처럼 직접 값을 설정해줄 수도 있다.

3. 비동기로 데이터를 가져오니 화면에서 데이터를 렌더링 해주기 전에 대기시간을 주자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
setTimeout(() => {
const { datas } = state;

$tabs.style.setProperty("--tabs-length", datas.length);
document.querySelector(".spinner").style.display = "none";

$tabs.innerHTML = `
<nav>
${datas
.map(
(data, index) =>
`<div class="tab" data-index="${index}">${data.title}</div>`
)
.join("")}
<span class="glider"></span>
</nav>
${datas
.map(
(data, index) => `<div class="tab-content ${index === 0 ? "active" : ""}">
${data.content}
</div>`
)
.join("")}
`;
}, 1000);
  • 위 코드는 리팩토링을 하면서 조금더 역할을 나눠서 수정해봐야겠다.

4. template literal로 연산을 하려면 다음과 같이 해야한다.

1
2
3
4
5
6
7
8
9
10
$tabs.addEventListener("click", (e) => {
if (!e.target.matches(".tab")) return;

document.querySelector(".glider").style.transform =
"translateX(" + e.target.dataset.index * 200 + "px)";

document.querySelector(".glider").style.transform = `translateX(${
e.target.dataset.index * 200
}px)`;
});
  • 처음에 연산결과를 ${}안에 넣지 않고 document.querySelector('.glider').style.transform = ${e.target.dataset.index} 200 px 이런식으로 했는데 이렇게 하면 안되고 위처럼 해야한다.

Toaster 구현 중 느낀점

1. toastBox 재사용을 위해 함수로 만들었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const newToastBox = (type, title, Message) => {
const $div = document.createElement("div");

$div.classList.add("toast", type);
$div.innerHTML = `
<h4 class="toast-title">${title} ${
[...document.querySelectorAll("body .toast")].length
}</h4>
<div class="toast-message">
<svg width="24" height="24">
<use xlink:href="#${type}" />
</svg>
<p>${Message}</p>
</div>
<a class="toast-close">&times;</a>`;

$div.style.bottom = "0";
return $div;
};
  • ToastBox 를 재사용하기 위해 함수를 사용하는 것을 적절했으나, 함수가 너무 많은 역할을 하고 있다. 렌더링부터 div 생성과 반환을 함께 하고 있어 리팩토링이 필요하다.

2. render 함수는 화면에 rendering 해주는 역할을 한다.

1
2
3
4
5
6
7
const render = () => {
[...document.querySelectorAll("body .toast")].forEach((toast, index) => {
toast.style.bottom =
([...document.querySelectorAll("body .toast")].length - index - 1) * 100 +
"px";
});
};
  • 위 render 함수는 토스트 요소에 bottom style 값을 할당해주는 역할을 하고 있으므로, 리팩토링이 필요하다.

Autocomplete 구현 중 느낀점

1. e.target.matches() vs e.target.closest()

1
2
3
4
5
document.querySelector("body").addEventListener("click", (e) => {
if (!e.target.closest(".autocomplete")) {
document.querySelector(".autocomplete-suggester").classList.add("hide");
}
});
  • 해당 요소 바깥쪽을 클릭하였을 때 .autocomplete-suggester에 hide 클래스를 추가해주는 로직이다.

  • e.target.closest(‘.autocomplete’)는 이벤트 발생한 요소의 상위 요소를 찾아가면서 인자로 넘겨준 요소를 찾고 반환한다.
    만약 없다면 null 값을 반환한다.

  • 하지만, e.target.matches(‘.autocomplete’)는 이벤트 발생한 요소가 인자로 넘겨준 요소를 가지고 있는지 판단하여 불리언값을 반환한다.

matches()의 경우 이벤트 전파를 캐치하기가 어려우므로 상황에 따라 적절하게 사용하자.

  1. 방향키로 포커스 이동시 스크롤 이상하게 내려오는 현상
1
2
3
4
5
6
7
8
9
10
11
12
13
14
$suggestList.addEventListener("keydown", (e) => {
if (e.key === "ArrowDown") {
e.preventDefault();
e.target.nextElementSibling === null
? $suggestList.firstElementChild.focus()
: e.target.nextElementSibling.focus();
}
if (e.key === "ArrowUp") {
e.preventDefault();
e.target.previousElementSibling === null // prettier-ignore
? $suggestList.lastElementChild.focus()
: e.target.previousElementSibling.focus();
}
});
  • 방향키로 포커스 이동시 한번 이동할 때 마다 스크롤도 같이 내려와서 어색하게 동작하는 것을 방지하지 위해 방향키 값이 “ArrowDown”, “ArrowUp” 일 때만 e.preventDefault(); 를 해주었다.

  • focus() 메서드를 사용하여 선택 요소에 포커스를 선택해주었다.

3. lodash로 debounce 구현 제대로 하기

1
2
3
4
5
6
$searchInput.addEventListener(
"keyup",
_.debounce(() => {
setState({ inputValue: $searchInput.value });
}, 500)
);
  • 위와 같이 “keyup” 이벤트 발생 시 lodash debounce 메서드를 이벤트 핸들러로 등록해줘야한다.

  • 또한, debounce의 콜백함수에 실행할 코드를 입력해줄 때, setState함수가 인수를 받는다면 위와같이 써줘야한다.

단, 인수를 받지 않는다면 _.debounce(setState,500) 이런식으로 사용할 수 있다.

4. 사용자가 입력한 값과 countryCode를 state로 관리하였다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let state = {
inputValue: "",
countryCode: [],
};

const setState = (newState) => {
state = { ...state, ...newState };
const regExp = new RegExp(`${state.inputValue}`, "i");
state.countryCode = countryCode.filter((country) => country[1].match(regExp));
state.countryCode = state.countryCode.map((country) => [
country[0],
country[1].replace(regExp, `<strong>${country[1].match(regExp)}</strong>`),
]);
render();
};
  • 사용자가 입력한 값을 state로 관리하여 그 값과 countryCode의 1번째 요소인 나라이름과 일치하는 부분을 찾아 strong 태그로 감싼 값으로 바꿔주었다.

  • strong 태그 대신 b 태그를 사용하면 안된다.

댓글 공유

  • page 1 of 1

loco9939

author.bio


author.job