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 태그를 사용하면 안된다.

댓글 공유

페어프로그래밍

하나의 컴퓨터로 두 사람이 프로그래밍 작업을 진행한다. 작업 속도는 더디겠지만, 서로의 생각이 달라 배울점이 많을 것이다.

주의사항

  1. 개발 구현에만 급급하지 말자. 만드는 것보다 어떻게 코드를 만드는 것이 좋은지를 복습하라.
  2. 안되면 원인을 분석해서 원인을 해결하고 돌파해야한다.
  3. 중복 제거
  4. 네이밍 중요
  5. ESlint, prettier 일관성있게 유지하기가
  6. 요구사항 읽고 코드 짜기
  7. 회고를 하루에 30분씩 가져서 갈등해소 및 진행 방향 계획 및 수정

Tip

일단 요구사항에 충족하여 되게끔만든다. 이후 리팩토링을 시작하는데 요구사항 충족에 쏟은 시간의 최소 2-3배의 시간을 더 들여서 리팩토링을 한다.

네비게이터와 드라이버

네비게이터 : 전체 지도를 보며 목적지의 길을 안내한다. 운전 과정 중 의문이 드는 점 혹은 잘못 흘러가는 상황 바로 잡는다.

드라이버 : 직접 운전을 하는 역할, 현재 진행하는 것을 말로 표현하며 진행하고 네비게이터와 대화하며 진행한다.

  • 1시간 이내에서 서로 역할을 바꿔보자.

lodash로 throttle 구현하기

lodash를 CDN으로 설치하면 const _ = require('lodash') 해주지 않아도 된다.

1
2
3
4
5
const throttle = _.throttle(() => {
console.log("throttle");
}, 1000);

throttle(); // 1초 후에 'throttle' 콘솔창에 찍힌다.

옵션으로 leading(초기), trailing(후기)에 이벤트 캐치할 지 선택할 수 있다.
기본값은 둘다 true인데 만약 이벤트 발생하고 몇초 지난 뒤 콜백함수 호출하고 싶다면, { leading : false }를 3번째 인자로 넘겨주면 된다.

localStorage 로컬 스토리지

브라우저에 제공하는 로컬 스토리지를 사용하면 키, 값을 쌍으로 객체처럼 데이터를 저장할 수 있다.

  • 이 데이터는 브라우저를 새로고침하거나 껐다 켜도 유지된다.
1
localStorage.getItem("isDark");
  • 로컬 스토리지의 ‘isDark’ 키 값을 가져온다.
1
localStorage.setItem("isDark", true);
  • 로컬 스토리지의 ‘isDark’키 값에 true 값을 바인딩한다.

    이 때, 값은 문자열로 감싸져서 저장된다.

문자열로 저장된 true값을 불리언 true로 변환해주기 위해 다음과 같은 방법을 사용할 수 있다.

1
JSON.parse(localStorage.getItem("isDark"));

matchMedia로 시스템 다크모드 확인

시스템모드가 다크모드인지 아닌지를 확인할 수 있는 방법이 있다.

1
window.matchMedia("(prefers-color-scheme: dark)").matches;
  • 만약 시스템 다크모드가 true면 위 값이 true가 나오고 시스템 모드가 라이트모드면 dark 대신 light를 넣어주면 true가 나온다.

댓글 공유

loco9939

author.bio


author.job