페어프로그래밍 2 최종 회고

코드의 가독성

  • 코드가 간단하거나 짧다고 가독성이 무조건 좋은 것은 아니다. 중복되는 코드가 있더라도 가독성이 좋을 수 있다.

  • 클린 코드란 것은 없다. 다만 좋은 코드란, 맥락을 모르는 다른 사람이 쉽게 읽을 수 있도록 코드를 작성하는 것이 중요하다.

  • 가독성이 좋아지려면 같은 역할을 하는 것들 끼리 모아두는 것이 가독성을 높인다.

  • 현업에 가게되면 지금처럼 코드에 대한 퀄리티를 생각할 여건이 조성되지 않을 수 있다. 그러니 지금부터라도 좋은 습관을 가지는 것이 중요하다.

큰 일을 해결하면 작은 일은 알아서 해결되리라

  • 작은 부분에 몰두하여 그것을 해결하려 하지 말고 요구사항을 보고 큰 부분부터 해결해나가자.

  • 전체의 구조를 한곳에서 모아서 생각하지 말고 각자의 역할을 생각하며 코딩을 하자.

ex) 데이터를 변경하면 setState 함수가 실행된다 -> setState 함수가 실행되면 render가 실행된다….

Test의 중요성

  • 리팩터링을 할 때는 test를 꼭 같이 하면서 진행해야한다.

  • 리액트를 배우고 나면 꼭 같이 배워보자!

댓글 공유

1. scrollSide

불필요한 서버 요청을 최소화 하자.

  • 토글이 될 때마다 로컬스토리지에 저장되는 것은 서버에 불필요한 요청이므로 사용자가 브라우저를 종료할 때, 로컬에 저장되도록 한다. 이와 마찬가지로 서버에 데이터를 가져올 때도 가져온 데이터를 변수에 담아서 그 변수를 사용한다.
    • 서버에 요청을 최소화하자!
    • 서버 요청을 최소화하기 위해 localStorage에서 값을 가져올 때는 DOMContentLoaded 이벤트가 발생할 때 가져온다.
    • localStorage에 값을 저장할 때는 페이지를 나가거나 새로고침했을 때 발생하면 되므로 beforeunload 이벤트 핸들러를 사용하였다.

2. tictactoe

얕은 복사를 하게 되면 접근자 프로퍼티가 제대로 복사되지 않는다.

  • getWinner 함수는 상태값을 가지고 winner가 있는지? 있다면 누구인지를 반환해준다. state에서만 사용되는 함수이므로 접근자 프로퍼티로 사용하여 응집도를 높이는 것이 좋다고 판단하였다.

    • 하지만, setState에서 newState로 재할당이 이루어지면서 접근자 프로퍼티가 제대로 복사되지 않는 문제가 발생하였기 때문에 다시 원래대로 돌려놓았다.
    • 아니면 재할당을 할 때, 접근자 프로퍼티를 제외한 즉, key,value 쌍이 있는 값만 재할당을 해주는 방법도 있다.
  • 즉시실행함수로 감싸서 정보은닉을 하려고 하였지만 정보 은닉만을 위해서 tictactoe를 전부 즉시실행함수로 만드는 것은 어색하다. 왜냐하면 즉시실행함수가 root 컨테이너를 참조하고 있기 때문에 독립적이라고 할 수 없는데 응집시켜놓은 것으로 보여지기 때문이다.

    • tictactoe에 Container를 건네주고 컴포넌트로 만들었다.

3. accordion

  • toggle 함수에서 옵션을 주는 경우와 기본값인 경우를 리팩토링으로 나눠보려고 하였는데 더 나은 방법이 떠오르지 않아 그냥 두었다.

4. drag & drop

  • dragleave 이벤트와 dragenter 이벤트가 거의 동시에 발생하는데, draggable 요소의 자식 요소에 마우스 포인터가 될 때도 두 이벤트가 발생하여 over 클래스가 붙혔다 떨어져 깜빡이는 현상 발생
    • dragleave 이벤트를 제거하고 dragenter 이벤트에 toggle을 달아주어 querySelectorAll(’li’)로 개선하였다.
    • dropzone 바깥에서 drop을했을 때 over 클래스가 제거되지 않는 문제가 발생하였는데 dragleave 이벤트를 제거했기 때문이다. dragend 이벤트로 대신 구현하였다.
    • dragleave 와 dragenter 발생시 toggle 메서드로 달아주는 방법도 있다.

5. analog clock

  • 숫자배열 하드 코딩하는 것 보다 array.from 을 사용하면 각 요소에게 콜백함수를 줘서 map 함수 사용을 제거할 수 있다.
  • timeToDeg 함수의 return 값을 객체형태로 바꿈
    • 인자가 많아지면서, 구조분해 할당 할 시 더 안정적임

6. star rating

  • querySelectorAll을 변수에 미리 담아줌
    • 렌더링이 한번만 되고 이벤트 발생시마다 DOM API를 호출할 필요가 없기 때문
  • array.from 콜백처리하여 map을 제거

7. calendar

  • background 클릭시 모든 캘린더를 hidden 하는 역할은 input이나 calendar에 종속된 역할이 아님
  • 상태들을 state 객체로 묶어서 관리
    • state 변경 함수를 거치지 않고 수정하는등의 부수효과를 막기위해
  • 이벤트 핸들러 내의 내용을 함수로 추출함
    • 이벤트 핸들러는 간단히 흐름을 파악할 수 있게 해주고, 자세한 동작은 함수에 정의하는 것이 가독성이 좋다고 생각함.

8. newsviewer

  • viewport를 줄여서 observer-target이 한 화면에 보이면 intersectionObserver API에 문제를 발생한다. scrolling해도 데이터를 가져오지 못한다.
    • 뷰포트 크기를 계산하여 window.innerHeight와 observer-target의 Y를 계산하여 나눈 값만큼 데이터를 fetch 시켜줘서 화면 비율이 크든 작든 뷰포트에 맞게 데이터를 가져와야한다. 즉, viewport가 확대된 상태면 Math.floor(window.innerHeight / observer-tagetY) 갯수만큼 fetch를 시켜줘서 화면크기에 맞게 초기 렌더링을 해줘야 한다.
    • element.getBoundingClientRect() 로 element의 뷰포트 위치를 구할 수 있다.

나의 부족한 점

  • 어떤 문제가 발생했을 때, 문제의 원인을 찾지 못한다.
    • 해당 문제가 발생한 원인을 제대로 알지 못하여 어떤 부분을 수정해야할지 찾기 어렵다.
  • 가치판단이 잘 서지 않는다. A, B가 있을 때 어떤 방법이 더 나은 것인지 선택하기 어렵다. 기준이 나의 생각이라고는 하는데, 남의 얘기를 들어보면 그것도 맞는 것 같다.

소감

내일이면 페어프로그래밍2도 끝난다. 페어1을 할 때보단 그래도 조금 실력이 늘었다고 생각하지만, 아직 전체적인 틀을 생각하는 연습이 필요하다고 생각했다. 또한, 주어진 요구사항에 맞는 한가지일을 구현하려고 하지 않고 다른 환경까지 고려하는 행동때문에 코드 작성하기를 망설이는 행동도 조금 나아져서 다행이다.

나보다 훨씬 잘하는 짝과 함께 해서 진도도 빠르게 나가고 배운 것이 많아서 좋았다. 서로 의견을 편하게 말하면서 코딩을 하다보니 재미도 경험도 더 많이 쌓을 수 있어서 좋은 경험이라고 생각한다. 다만 내가 새로운 지식을 얻기 위해서는 내가 가진 지식을 100% 이해하고 다른 사람에게 설명할 수 있을 정도로 알고 있어야 하므로, 내가 배운 지식을 기록만 하지말고 최대한 다른 사람과 의견을 공유하는 방향으로도 나가봐야겠다고 생각이 들었다.

나중에 회사에 취업하게 되어도 이런 개발 환경이 갖춰진 회사에서 열심히 배워서 역량을 키워보고 싶다!

댓글 공유

1. drag 상태 관리

dragstart의 origin과 drop의 target 노드를 전역 상태에서 관리하면 setState로 변경될 때마다 리렌더링 발생한다. 요구사항에서는 DOM 변경만 하고 drop 했을 때 상태를 변경하라고 하였으니 전역 상태로 관리해주면 안된다.

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
class List extends Component {
constructor(id) {
super();
this.id = id;
this.$origin = null;
this.$target = null;
}

addEvent() {
return [
this.createEvent({
type: "dragstart",
selector: ".list-container",
handler: (e) => {
this.$origin = e.target.closest(".list-container");
e.dataTransfer.effectAllowed = "move";
},
}),
this.createEvent({
type: "dragenter",
selector: ".list-container",
handler: (e) => {
if (this.$origin !== e.target.closest(".list-container")) {
this.$target = e.target.closest(".list-container");
this.swap(this.$origin, this.$target);
}
},
}),
];
}
}
  • drag & drop이 발생하는 컴포넌트에서 지역 변수로서 관리해주면 된다.

2. 이벤트 등록 조건 수정

각자의 컴포넌트들을 window에 등록해줄 때, 이벤트 위임이므로 걸러주기 위해 matches를 사용하였는데, 이 경우 이벤트가 발생한 요소만 해당되고 이벤트 발생한 부모 요소를 찾을 수 없어 list 요소에 drag 이벤트가 발생하지 않는 문제가 발생하였다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Component
checkEvent() {
if (this.addEvent) {
this.addEvent().forEach(({ type, selector, handler }) => {
if (events.find(event => event.type === type && event.selector === selector)) return;

if (selector === '') {
window.addEventListener(type, e => handler(e));
} else {
window.addEventListener(type, e => {
if (!e.target.matches(selector) && !e.target.closest(selector)) return;
handler(e);
});
}

events = [...events, { type, selector, handler }];
});
}
}
  • 자신을 포함한 부모쪽에서 selector를 찾을 수 있기 때문에 closest도 추가해준다.

3. DOM만 변경하고 상태는 변경시키지 않는 경우

list 드래그 시 dragenter가 발생할 때, DOM을 직접 변경하여 화면에 list가 서로 바뀐 것처럼 보이도록 구현하였다. 하지만, drop을 발생시켰을 때, 1번 list에서 2번 리스트로 dragenter 되었다가 다시 1번 list로 드래그 한체로 돌아와서 drop을 하면 DOM 상에서 바뀌지 않았는데, 상태가 바뀐 것으로 감지하여 상태 lists의 순서를 변경하는 문제가 발생하였다.

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
// state
const arrangeList = (idOrder) => {
console.log(state);
setState({
lists: state.lists.sort((a, b) => {
const aOrder = idOrder.findIndex((id) => id === a.id);
const bOrder = idOrder.findIndex((id) => id === b.id);
return aOrder < bOrder ? -1 : aOrder > bOrder ? 1 : 0;
}),
});
console.log(state);
};

// List Component
class List extends Component {
addEvent() {
...
this.createEvent({
type: 'drop',
selector: '.list-container',
handler: e => {
e.stopPropagation();
arrangeList([...document.querySelectorAll('.list-container')].map($c => +$c.id));
return false;
},
}),
}
}
  • dragenter 이벤트 핸들러에서 DOM을 직접 변경해주었기 때문에, DOM이 변경되었는지 확인하기 위해서는 DOM API를 사용할 수 밖에 없었다.

  • DOM에 직접 접근하여 drop 이벤트가 발생하였을 때, DOM의 list-container 요소들을 가져와서 id로 배열을 반환하여 인수로 전달해주었다. 이 때 전달한 인수는 화면에 나타난 list의 순서를 의미한다.

페어프로그래밍2의 목적

이번 페어프로그래밍2의 목적은 리액트 프레임워크를 배우기 전에 컴포넌트가 무엇인지 제대로 알고 가기 위함이였다.

컴포넌트란?

모든 컴포넌트는 자신이 어디에 그려질지 알면 안된다.

render가 알아야 한다.

  • 컴포넌트는 어떤 모습으로 그려질 것인가에 대한 정보를 가지고 있다.
  • render가 그 정보들을 가지고 그려주는 역할을 한다.
  • App 컴포넌트가 자식 컴포넌트까지도 알아야 그려줄 수 있다.
  • 부모 컴포넌트가 자식컴포넌트를 알고 있으면 새롭게 그려질 정보와 이전의 그려진 정보를 비교하여 효율적인 렌더링이 가능하다.

컴포넌트가 부모요소의 컨테이너를 받아서 그곳에 innerHTML로 그리면 이는 부모가 그린 HTML에 종속되는 것이므로 컴포넌트를 독립적으로 사용할 수 없게되므로 컴포넌트 개념을 벗어난다.

효율적인 렌더링을 위해서…

효율적인 렌더링을 위해서는 diff 알고리즘을 구현하였었다. 만약 App 컴포넌트가 일부분을 그리고 나머지 부분을 다른 컴포넌트가 받아서 그리는 하향식 구조로 설계를 하게된다면 diff 알고리즘이 하향식으로 비교해나가는데 이는 어디가 최하단인지 판별해주기가 까다로워진다.

그러므로 재조정은 App 컴포넌트가 해야한다. App 컴포넌트는 다른 컴포넌트에게 그들을 그리기 위한 정보를 전달받아 이것들을 모아서 render 함수로 보내서 화면에 렌더링을 시켜준다. render 함수는 그려주는 역할을 하고 App 컴포넌트는 그려지기위한 정보를 모두 가지고 있어야한다.

소감

  • 코드 작성에 대해 어떤 생각을 가지고 구현했는지 명확하게 얘기할 수 있으면 Ok
    • 되는대로 짜는 것이 아니라 명확한 이유가 가지고 짜면 위의 문제가 해결
  • 내가 작성한 코드는 나만 잘 알고 있다. 상대방에게 설명할 때, 상대방이 어느정도 알고 있을 것이라는 생각은 접어두자.

댓글 공유

trello Tip

카테고리 Pair Programming, level 2

1. trello 상태관리

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
let state = {
lists: [
{ id: 1, title: "Frameworks" },
{ id: 2, title: "OS" },
],
cards: [
{
id: 1,
content: "React",
boardId: 1,
},
{
id: 2,
content: "Vue",
boardId: 1,
},
{
id: 3,
content: "Windows",
boardId: 2,
},
{
id: 4,
content: "MacOS",
boardId: 2,
},
],
};
  • 처음에는 위와 같은 방식으로 상태를 관리해주려 하였다.trello cards를 드래그하여 다른 list로 옮겼을 경우도 있기 때문에, trello list에 cards가 종속되어 있지 않다고 생각했기 때문이다.

  • 하지만 리스트 내의 cards가 순서대로 등록되기 때문에 list 별로 cards를 배열로 관리하는 것이 좋다고 판단하여 다음과 같이 상태를 변경해주었다.

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
let state = {
listOriginId: null,
isListMakerOpen: false,
lists: [
{
id: 1,
title: "Frameworks",
isOpen: false,
cards: [
{
id: 1,
content: "React",
},
{
id: 2,
content: "Vue",
},
],
},
{
id: 2,
title: "OS",
isOpen: false,
cards: [
{
id: 3,
content: "Windows",
},
{
id: 4,
content: "MacOS",
},
],
},
],
};
  • 추가로 렌더링에 영향을 끼치는 데이터는 모두 상태로 관리해주는 것이 DOM API를 적게 사용하고 동적 HTML에 선언적으로 style을 주거나 데이터를 바인딩해줄 수 있어 훨씬 가독성이 높아진다.

때문에, DOM API 사용을 자제하고 만약 DOM API를 사용해야 하는 경우라면 꼭 한번 고민해보고 사용하도록 하자.

2. diff 알고리즘은 프로퍼티까지 비교해줘야한다.

이전에 diff 알고리즘을 구현하였을 때, attribute 값만 서로 비교해주고 property 값은 비교해주지 않았다. textarea의 value 값을 빈값으로 바꿔주고 싶어서 이를 상태로 관리해야하나? 생각이 들어 상태로 관리해보려했다.

하지만, textarea의 attribute인 value는 처음부터 빈값이였고 input 이벤트가 발생하고 이 때의 value값(property 값)을 상태에 등록해주더라도 diff 알고리즘이 attribute만 비교해주기 때문에 처음과 input이벤트 발생한 후의 textarea는 변한게 없는 것으로 간주하였다.

그러므로 diff 알고리즘에서 property까지 비교하여 바뀐 부분이 렌더링 되도록 알고리즘을 개선해주었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// property 비교 추가
const diff = ($new, $old) => {
const newNodes = [...$new.childNodes];
const oldNodes = [...$old.childNodes];

...

newNodes.forEach(($n, i) => {

...
// 원래는 프로퍼티를 전부 비교해줘야하지만, 사정상 value, checked만 확인해주겠다.
const $o = oldNodes[i];
if ($n.value !== $o.value) {
$o.value = $n.value;
}
if ($n.checked !== $o.checked) {
$o.checked = $n.checked;
}
});
};
  • 그 결과 textarea가 변경될 때마다 textarea의 value property가 변경되므로 재렌더링이 발생하므로 요구사항에 맞게 구현할 수 있다.

3. drag & drop 기능 완벽하게 이해하기

요구사항에서 drag한 list를 드래그 한체로 다른 list 위로 올렸을 때, 상태가 변경되지 않고 DOM을 직접 변경하여 list가 서로 swap 되도록 하고 drop한 이후에 state를 변경하라고 적혀있다.

각자의 이벤트 핸들러는 서로의 event target을 알 수 없으므로 상태로 관리해줘야만 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
let state = {
listOriginId: null,
targetId:null,
...
}

window.addEventListener('dragstart', e => {
state.listOriginId = "drag 시작한 list의 id"
})

window.addEventListener('dragenter', e => {
state.targetId = "dragenter 발생한 list의 id"
swapList(e.target.id) // DOM 직접 변경하여 dragstart 이벤트 발생한 노드와 dragenter 이벤트 발생한 노드를 변경
})

window.addEventListener('dragend', e => {
setState({...}) // listOriginId, targetId를 가지고 lists의 배열 요소 위치를 서로 바꿔준다.
})

// drop 이벤트 발생시키기 위해서 dragover 이벤트 핸들러에 e.preventDefault()
window.addEventListener('dragover', e => {
e.preventDefault();
})

  • 사실 drop 이벤트를 발생시키기 위해서 dragover 이벤트에서 e.preventDefault()를 호출하였는데, drop 이벤트를 사용하지 않아도 구현이 가능하여서 제거해도 될까 했지만, 제거하게 되면 dragend가 발생했을 때, dragstart한 요소가 제자리로 돌아오는 트랜지션(?)이 제거되지 않으므로 남겨두었다.

  • drop 이벤트를 사용하지 않은 이유는 drop 이벤트는 draggable인 요소에서만 drop 이벤트가 발생한다. 즉, dragenter로 새로운 list랑 이전 list의 DOM을 변경해주었는데, drop을 list 요소 바깥에서 발생시키면 drop 이벤트가 발생하지 않는 문제점이 있어서 drop 이벤트 대신에 dragend 이벤트를 사용하였다.

소감

  • if문은 어렵다. input 값이 빈칸일 때, a조건 또는 b조건 만족해야지 실행하는 예제를 한줄로 줄여보려 했지만 복잡해서 일단 따로 사용했다.

  • state는 객체이고 그 안에 여러 상태 데이터를 관리하는데, lists를 배열로 관리해주고 있고, lists 배열의 요소를 list라고 한다면 list는 객체로 관리되고 list 안에 또 cards라는 배열이 있고 cards 배열의 요소는 card라는 객체이다. 이런 복잡한 구조의 state를 가지고 고차함수를 사용하는데 어려움이 있었다.

댓글 공유

2. tictactoe

  1. 재렌더링 일어나는 부부은 game-status, game-grid-items 이 부분만 인데, innerHTML로 game 안을 전부 변경하려고 하니 재렌더링 필요 없는 부분인 game-reset 까지 재렌더링 해줘야하는 문제가 생겼다.
    1. 일단은 재렌더링 일어나도록 구현하였다.
  2. isOver라는 상태를 두는 것이 맞는지? 이벤트 핸들러 안에서 isOver 값을 가지고 얼리리턴으로 더이상 게임을 못하게 막는것이 맞는지…
    1. 일단 위와같이 하였다.

4. drag

  1. swapLang 할 때, 왜 새로운 변수에다가 langs 값을 복사해서 다루는 것인가?
    1. lang을 변경할 수 있는 역할을 하는 함수가 setLang 함수이기 때문에 lang 데이터 변경은 반드시 setLang 함수로만 해야하기 때문이다.
  2. render에서 lang 변수를 받는 이유는 ??
    1. 함수를 순수함수로 만들어주기 위함이다. 매개변수로 받은 인자를 가지고만 코드를 실행하도록 짜서 순수함수가 되도록 한다.
  3. dragenter 이벤트로 드롭할 요소로 드래그 했을 때, p태그, i태그 등 div안의 다른 요소에 드래그한 체로 이동하면 dragleave이벤트랑 겹침 현상이 발생하여 이를 해결하기 위해서는 얼리 리턴으로 조건을 달아줘야하는데, 복잡한 구조로 되어있는 경우 매우 어려워진다. 그러므로 dragover를 사용하여 이벤트가 많이 발생하는 것은 디바운스로 해결해주자.

소감

혼자서 짜보려고 하니 같이 짜는 것보다 스스로 생각해보고 문제를 해결해나가는 능력이 길러진 것 같다. 같이 짜면 상대방에게 의존할 수 있는 경우가 있는데 혼자서는 그런 것 없이 혼자 다 이겨내야하므로 어렵지만 더 기억에 오래 남을 것 같다. 앞으로도 내가 직접 해본것이 아니라면 반드시 내 손으로 다시 직접 해보면서 해결방안을 직접 터득하면서 나아가야겠다.

댓글 공유

diffing 구현

React에서는 화면에서 바뀐 부분만 부분적으로 렌더링 해주기 위해 diffing 알고리즘을 구현하였다.

virtualDOM과 realDOM을 비교해주기 위해 리액트 홈페이지에서 설명한 방법으로 vanilla JS로 diffing 알고리즘을 구현해보자.

0. 두 요소의 길이부터 비교한다.

우리가 작업하고 있는 요소를 $old, 새롭게 생성된 요소를 $new 요소라고 하자. 우리는 $new를 기준으로 $old의 요소들을 일치 시켜주는 행위를 하면 된다.

$new의 노드길이가 $old의 노드 길이보다 길다면, $old에서 추가해주면 되지만, $new가 $old보다 길이가 짧다면, $old의 나머지 부분을 제거해줘야한다.

0.1. 길이가 짧은 경우

1
2
3
4
5
6
7
8
9
const diff = ($new, $old) => {
const newNodes = [...$new.childNodes];
const oldNodes = [...$old.childNodes];

if (oldNodes.length > newNodes.length) {
for (let i = 0; i < oldNodes.length - newNodes.length; i++)
$old.removeChild($old.lastChild);
}
};

0.2. 길이가 긴 경우

1
2
3
4
5
6
7
8
newNodes.forEach(($n, i) => {
const $o = oldNodes[i];

if ($o === undefined) {
$old.appendChild($n.cloneNode(true));
return;
}
});
  • 이전 요소에 새로운 요소가 가진 자식 노드가 없다면 이전 요소에 추가해준다.

cloneNode(true)는 해당 요소의 자식노드들 까지 모두 복제해주기 위한 옵션이다.

1. 두 요소의 루트 노드부터 비교한다.

먼저 두 루트 요소의 타입이 다르면 $old 요소에서 제거하고 새로운 트리를 구축한다.

ex) 에서

로,
에서 로 바뀌는 경우

1
2
3
4
5
6
7
8
newNodes.forEach(($n, i) => {
const $o = oldNodes[i];

if ($o.tagName !== $n.tagName) {
$old.replaceChild($n.cloneNode(true), $o);
return;
}
});
  • 이전 요소의 자식노드과 새로운 요소의 자식노드를 1:1로 tagName을 비교하여 서로 다르면 교체를 해주었다.

2. textNode, commentNode 들은 textContent를 비교한 뒤 교체해준다.

앞서 tagName으로 비교하여 같지 않을 때, 자식노드까지 비교해주었는데, textNode와 commentNode는 서로 tagName이 같으므로 이들의 textContent로 비교해주어야 한다.

1
2
3
4
5
6
7
8
9
newNodes.forEach(($n, i) => {
const $o = oldNodes[i];
// textNode:3, commentNode:8
if ($n.nodeType === 3 || $n.nodeType === 8) {
if ($n.textContent !== $o.textContent)
$old.replaceChild($n.cloneNode(true), $o);
return;
}
});

3. 이제 각 노드의 attribute 노드를 비교한다.

2번까지의 과정으로 $old 요소의 자식노드와 $new 요소의 자식노드들의 비교가 끝났고 일치까지 시켜줬다. 그러므로 이제는 자식 노드가 가지고 있는 attribute(class, inline-style, checked, disabled…)를 비교해줘야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
newNodes.forEach(($n, i) => {
const $o = oldNodes[i];

const newAttrs = [...$n.attributes];
const oldAttrs = [...$o.attributes];

for (const $nAttr of newAttrs) {
const $sameAttr = oldAttrs.find(($oAttr) => $oAttr.name === $nAttr.name);

if ($sameAttr === undefined) {
$old.replaceChild($n.cloneNode(true), $o);
return;
}

if ($sameAttr.value !== $nAttr.value) $sameAttr.value = $nAttr.value;
}

oldAttrs.forEach(($oAttr) => {
if (!newAttrs.find(($nAttr) => $nAttr.name === $oAttr.name))
$o.removeAttribute($oAttr.name);
});

diff($n, $o);
});
  • 새로운 노드의 attritube 중 이전 노드의 attribute와 같은 attribute가 없다면, 이전 요소 노드의 attribute를 새로운 노드의 attribute로 추가해준다.

  • 이전 노드의 attribute 값과 새로운 노드의 attribute 값이 다르면 이전 노드 attribute에 값을 할당해준다.

  • 만약 이전 노드의 attribute 중 새로운 노드의 attribute가 없다면 이전 노드의 해당 attribute를 제거해준다.

그리고 마지막으로 자식 노드들을 재귀적으로 diff 함수를 호출해주면 자식 노드까지 바뀐 부분만 확인해줄 수 있다.

소감

  • diff 알고리즘을 경현님과 같이 이야기 하면서 작성하다보니 완성할 수 있어서 기뻤다.

  • 이전 노드의 attribute와 새로운 노드의 attribute가 다를 때, attribute를 추가해주기 위해 setAttribute()를 사용하였는데, attribute는 HTML 구조의 초기값을 설정해주는 것이므로, 사용자의 화면에 변화되는 것이 반영되지 않고 todo를 삭제할 때, 삭제한 다음 요소에 삭제 요소의 attribute가 남아있는 것처럼 보이는 문제가 발생하여 attribute를 추가하는 경우에는 replaceChild() 메서드를 사용하여 해당 요소의 노드를 바꿔주는 방식을 사용하였다.

때문에 attribute를 추가하는 경우에는 li 요소가 전부 렌더링 되는 모습을 보이고 attribute를 제거할 때는 input에 있는 checked attribute만 제거되므로 li 요소가 아닌 li 자식 요소의 input과 input의 다음 형제 요소들만 렌더링 되는 모습을 볼 수 있었다.

댓글 공유

CBD Library Tip

카테고리 Pair Programming, level 2

9. CBD

로직

  1. 처음 DOMContentLoaded 이벤트가 발생하였을 때, 초기 state 값 설정 및 render 함수를 실행한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
window.addEventListener("DOMContentLoaded", () => {
createState({
todos: [
{ id: 3, content: "Javascript", completed: false },
{ id: 2, content: "CSS", completed: true },
{ id: 1, content: "HTML", completed: false },
],
todoFilter: ["All", "Completed", "Active"],
currentTodoFilterId: 0,
});

render();
});
  1. render가 실행되면 App 클래스의 인스턴스를 생성하고 생성된 인스턴스를 domStr 프로토타입 메서드를 사용하여 문자열로 반환하고 이 문자열을 node(virtual DOM)로 반환해주었다. 생성된 Virtual DOM과 기존 index.html에 있는 root가 생성한 Real DOM을 비교하는 diff 함수를 실행한다.
1
2
3
4
5
const render = () => {
const app = new App();
console.log(app);
diff(app.newDOM(), $root);
};
  1. App 클래스에서는 todoInput, todoList, todoFilter 라는 인스턴스 프로퍼티에 각각 클래스형 컴포넌트의 인스턴스를 할당해주었다. 이 프로퍼티(컴포넌트)를 components 배열에서 관리를 해준다. 또한, 각 컴포넌트에서 데이터가 변경되어 재렌더링이 발생하면 2번 과정이 발생한다.
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
let components = []; // App 인스턴스 여러개 만들면 구분해주기 위해 전역에서 선언
class App {
constructor() {
components.forEach((c) => c.unmount());

this.todoInput = new TodoInput();
this.todoList = new TodoList();
this.todoFilter = new TodoFilter();

components = [this.todoInput, this.todoList, this.todoFilter];

components.forEach((c) => c.mount());
}

domStr() {
return `
${this.todoInput.domStr()}
${this.todoList.domStr()}
${this.todoFilter.domStr()}
`;
}

newDOM() {
return domStrToNode(this.domStr());
}
}
  • 이렇게 생성된 컴포넌트에 이벤트 핸들러를 등록시켜주기 위해서는 App 클래스의 인스턴스가 생성될 때, 즉 데이터가 변경되어 렌더링이 될 때 마다 기존 컴포넌트 목록들에게 unmount(이벤트 제거)를 실행하고 다시 컴포넌트를 등록하고 mount(이벤트 등록)을 실행한다.
  • 이벤트를 등록하고 제거해주어 컴포넌트의 생명주기를 관리해주는 이유는 이벤트 위임으로 window에게 이벤트를 등록만 하고 제거해주지 않는다면 재렌더링 발생하여 컴포넌트가 새로 생성되면 그 때 다시 또 이벤트를 등록해주므로 불필요한 이벤트 등록과 성능상 문제가 발생할 수 있기 때문에 재렌더링이 발생할 때 꼭 이전 컴포넌트의 이벤트를 제거해줘야한다.
  1. 각 컴포넌트에서는 이벤트 핸들러를 작성해주고 mount 함수 안에 이벤트 리스너를 등록하고 unmount 함수안에 이벤트를 제거해준다. domStr 함수는 해당 컴포넌트를 HTML 구조를 문자열로 반환해준다.
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
class TodoList extends Component {
constructor() {
super();

let todos = [...state.todos];
if (state.currentTodoFilterId === 1)
todos = state.todos.filter((todo) => todo.completed);
if (state.currentTodoFilterId === 2)
todos = state.todos.filter((todo) => !todo.completed);

this.items = todos.map(({ id }) => new TodoItem(id));
}

mount() {
this.items.forEach((item) => item.mount());
}

unmount() {
this.items.forEach((item) => item.unmount());
}

domStr() {
return `
<ul class="todo-list">
${this.items.map((item) => item.domStr()).join("")}
</ul>
`;
}
}
  • todoList 경우는 App 에서 todoList, todoInput, todoFilter 컴포넌트를 관리해주는 것처럼 todoItem 컴포넌트를 관리해준다. App 에서 mount 함수를 실행하여 각 컴포넌트의 mount, unmount를 통해 생명주기를 관리해준 것 처럼 todoList의 mount 에서는 todoItem 컴포넌트의 mount, unmount를 관리해준다.
  • this.items 에는 각 id에 해당하는 TodoItem의 컴포넌트들로 구성된 배열이 할당된다. 그리고 domStr 함수에서 TodoItem 컴포넌트들을 map 고차함수로 돌면서 각 컴포넌트들의 domStr 함수로 호출하여 문자열로 반환한다.
  • TodoFilter에서 domStr 부분에서 id가 꼭 필요한가?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    domStr() {
    return `
    <ul class='todo-filter'>
    ${state.todoFilter
    .map(
    (filter, i) =>
    `<li class=" filter ${i === state.currentTodoFilterId ? 'active' : ''}" id="${i}">${filter}</li>`
    )
    .join('')}
    </ul>
    `;
    }
    • all, completed, active 에 id가 굳이 필요한가??
  1. 각 컴포넌트에서 state로 관리하는 부분이 변경될 때, 상태가 바뀌는 부분만 재렌더링이 일어나도록 구현하였으므로 todoInput에서는 재렌더링이 일어날 부분이 없으므로 autofocus 기능이 사라지지 않는다.
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
class TodoInput extends Component {
constructor() {
super();

this.addTodo = (e) => {
if (e.key !== "Enter" || e.target.value.trim() === "") return;

state.todos = [
...state.todos,
{ id: getNextId(), content: e.target.value, completed: false },
];
e.target.value = "";
};
}

mount() {
window.addEventListener("keyup", this.addTodo);
}

unmount() {
window.removeEventListener("keyup", this.addTodo);
}

domStr() {
return `<input class='todo-input' autofocus/>`;
}
}

문제

  • checkbox 클릭하면 checked가 생성되고 사라지는 렌더링은 잘 되지만, 화면에서는 바뀌지 않는 문제가 발생하였다.
    • attribute와 property의 차이때문에 발생하는 문제점이라고 생각되었다.
    • setAttribute대신에 요소를 replaceChild()로 자식노드를 교체해주었다.

소감

  • 혼자서 공식문서들여다 보는 것 보다 확실히 옆에서 같이 이야기 하고 생각을 공유하면서 코드를 작성해나가다 보니 diff 알고리즘도 완성할 수 있어서 기뻤다.
  • node.children 은 텍스트 노드를 제외한 자식 노드들을 반환
  • node.childNodes는 모든 자식 노드를 반환한다.
  • attribute와 property의 차이점에 대해 한번 더 생각해볼 수 있는 시간이 있어서 좋았다. attribute는 HTML에 있는 속성으로 rendering이 되면 attribute는 DOM property로 변환된다. 하지만 1:1 매칭이 되지는 않는다는 점을 주의하자.
    • attribute : 변하지 않는 초기 default 값 전달
    • property : 사용자의 행동으로 변할 수 있다.
    • diff 구현할 때, removeAttribute는 제대로 동작했는데, setAttribute할 때는 checkbox값이 삭제했을 때, 다음 todo에 이전되는 문제가 발생하였는데 이는 따로 정리해둬야겠다.

댓글 공유

8. news viewer

  1. 전체적인 구조를 설명하면, Nav, NewsList는 컴포넌트로 관리하여 독립적인 요소로 관리하고 root 요소에 appendChild 해주었다.
  2. Nav 요소가 클릭되면 state 상태 데이터를 클릭한 요소의 id와 변경해준다. ⇒ Nav 요소는 상태를 변경 시키지만 리렌더링은 일어나지 않는다.
  3. state 상태 데이터가 변경이 일어나면 NewsList가 리렌더링이 발생한다.

Proxy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let state = new Proxy(
{ category: "all" }, // state 객체에 key,value로 들어가게된다.
{
set: (obj, key, value) => {
obj[key] = value;
window.dispatchEvent(
new CustomEvent("statechange", {
detail: obj,
})
);
return true; // setter 는 성공했음을 나타내기 위해 true를 반환해줘야 한다.
},
}
);

state[category] = "business"; // handler 객체의 setter 접근자 프로퍼티가 실행된다.
  • 프록시 객체를 생성하여 state(상태)가 변경, 할당이 발생하면 proxy의 handler에서 먼저 로직을 실행
  • setter 접근자 프로퍼티가 실행된다는 의미는 state(상태)에 변경이 일어난 경우이므로 이 때, window에게 custom event의 이벤트 객체를 보낸다.

Observer(Intersection Observer API)

옵저버 패턴은 이벤트를 발생 시킨 객체(subject)에 옵저버나 리스너를 등록하여서 이들이 subject를 관찰하면서 구독 혹은 구독 취소 같은 행위를 할 수 있도록 하는 패턴을 말한다.

  • 상태가 변경되면 옵저버에게 notify() 메서드로 알림을 보낸다
  • 옵저버(handler)는 subject가 변경될 때, 호출할 함수들이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let observer = new IntersectionObserver(
entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
currentNews = [...currentNews, ...getCategoryData(currentCategory)];
render(currentNews);
}
});
},
{
threshold: 1,
}
);

observer.observe($newsList.querySelector('.scroll-observer'));
};
  • spinner(’.scroll-obeserver’) 가 viewport에 지정한 임계점(threshold) 이상 나타났을 때, if문 안의 코드를 실행시킨다.
  • entries forEach로 조건식 entry.isIntersecting이 true 이면 처음 스크롤을 내려서 보였을 때 실행하고 false면 다시 스크롤을 올렸을 때는 코드를 실행하지 말라는 의미이다. (양방향으로 일어나기 때문에 관리해줘야한다.)
1
2
3
4
5
6
window.addEventListener("DOMContentLoaded", async () => {
observer.subscribe(render);
intersectionObserver = createObserver();
await render();
intersectionObserver.observe($container.querySelector(".scroll-observer"));
});
  • observe() 메서드를 사용하여 옵저버 타겟을 설정할 수 있다.
    • 이 코드는 DOMContentLoaded 이벤트가 발생했을 때, 한번만 발생하면 된다. 리팩터링 이전 코드는 render 할 때마다 spinner 요소를 재생성하여 위 메서드를 사용하여 target으로 지정하기 위해서 꼭 render 함수 내부에 있어야 했는데, 이는 불필요한 함수 호출과 리렌더링이므로 이를 해결하기 위해 렌더링이 불필요한 템플릿들은 App.js에서 $container innerHTML로 미리 할당해준다.
    • 위와 같이 await로 렌더가 될 때 까지 기다렸다가 ‘.scroll-observer’ 요소가 생성된게 보장이 되면 옵저버 타겟을 등록한다.
1
2
3
4
5
6
7
8
9
10
const addNews = (category, nextNews) => {
news = {
...news,
[category]: {
...news[category],
articles: [...news[category].articles, ...nextNews],
page: news[category].page + 1,
},
};
};
  • 기존에 있던 news 객체에 새로운 news 객체를 스프레드 문법으로 추가해줘야하는데, 추가해줄 때, 기존의 값을 스프레드 문법으로 풀어준 뒤 새로운 객체를 추가해줘야지 아니면 새로운 값만 할당된다.

9. CBD

1
2
3
4
5
6
class Component {
constructor($container) {
this.$container = $container;
observer.subscribe(this.render.bind(this));
}
}
  • this.render는 handler 내부에 있는 함수이름으로 호출하면 이는 일반함수로 호출되므로 this가 전역객체에 바인딩된다. 그래서 this가 불일치하는 현상이 발생한다. 이 때 bind 메서드를 사용하여 Component로 this를 바인딩한 새로운 함수를 전달해주므로 this를 일치시켜준다.

소감

  • 객체의 깊이가 길어지고 대괄호 표기법과 마침표 표기법 사용시점을 확실히 알지 못한다.
  • 구조분해 해줄 수 있는 것들은 구조분해 해줘서 간단하게 작성하자.
  • 클래스 new 연산자로 생성했을 때, 인스턴스의 구조가 어떤지 모른다. + super()
  • 8번 옵저버 패턴 구조 설명을 제대로 못한다.
  • $container를 잘못이해하고있다. 클래스 구조에서 받은 $container 매개변수를 컴포넌트로 단위로 구성할 것이다. InnerHTML과 혼동하고 있다. 컴포넌트로 사용할 컨테이너를 매개변수로 받는다.

댓글 공유

1. ToggleSideNav

1
2
3
4
5
6
7
8
const isOpen = {
get local() {
return JSON.parse(localStorage.getItem("isOpen"));
},
set local(value) {
localStorage.setItem("isOpen", JSON.stringify(value));
},
};
  • 접근자 프로퍼티 사용하면 함수를 사용하지 않아도 응집도를 높힐 수 있다.
1
2
3
4
5
6
7
8
9
10
window.addEventListener("DOMContentLoaded", () => {
if (isOpen.local === null) isOpen.local = false;

$nav.classList.toggle("active", isOpen.local);
});

window.addEventListener("load", () => {
document.body.style.visibility = "visible";
document.body.classList.remove("preload");
});
  • DOMContentLoaded 에서 CSS 속성을 설정해준 뒤 load 이벤트가 발생했을 때, body의 preload 클래스를 제거해줘야 요구사항대로 깜빡임 없이 작동한다. 왜냐하면 preload 클래스가 있으면 모든 자식요소의 transition 이벤트가 무효화된다.

2. tictactoe

  • 즉시실행함수로 클로저를 구현한다. ⇒ 정보은닉가능, 응집도 높이고, 관심사 분리
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
const people = {
a: [],
b: [],
};

// 승자를 가려내는 로직
Object.entries(people).forEach(([key, value]) => {
winning.some((w) =>
w.every((v, index) => v === value.sort((a, b) => a - b)[index])
);
});

// 리팩터링 후
const items = Array(9).fill(null);

// 승자를 가려내는 함수
const getWinner = () => {
const winning = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6],
];

for (let i = 0; i < winning.length; i++) {
const [a, b, c] = winning[i];
if (items[a] && items[a] === items[b] && items[a] === items[c]) {
return items[a];
}
}
return null;
};
  • people 객체 안에서 플레이어가 선택한 요소를 배열로 관리하니깐 depth가 깊어져서 가독성도 떨어지고 유지보수가 좋지 않다.
    • 요소를 클릭할 때 마다 getWinner() 함수 조건문을 판단한다. 그래서 items 배열에 0번째 인덱스부터 차례대로 player “O” or “X”가 삽입된다. 그런 다음에 승리의 경우의 수를 담은 winning 2차원 배열을 순회하면서 items 배열이 winning 배열과 같은지 비교해주고 있다.
      예를 들면, 처음에 X가 시작하면 X가 누른 요소의 data-id가 items의 index에 ‘X’가 할당되고 그 다음 ‘O’가 누른 요소의 data-id가 items의 index에 ‘O’가 할당된다. 그 때마다 반복문 돌면서 items 배열에 winning 배열의 인덱스에 모두 같은 player가 있는지 확인하여 승리 여부를 확인한다.
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
// 리팩터링 전
let current = true;
const people = {
O: [],
X: [],
};
let isWin = false;
let winner = '';

// 리팩터링 후
const state = {
player: 'O',
people: {
O: [],
X: [],
},
get isWin() {
const winning = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6],
];

return getCombinations(this.people[this.player], 3).some(c => winning.some(w => w.every((v, index) => v === c.sort((a, b) => a - b)[index])));
},
get winner() {
return this.isWin ? this.player : ' ';
},
get length() {
return Object.values(this.people).reduce((a, c) => a + c.length, 0);
},
};

// 2번째 리팩터링 후
let player = 'X';
let items = Array(9).fill(null);

const getWinner = () => {
...
};

const setPlayer = nextPlayer => {
...
};
  • 관리해야할 상태가 많아지면 부수효과 발생할 가능성이 높아지므로, 상태 최소화를 해주었다.
    • 리팩터링 후 상태값은 줄고 접근자 프로퍼티를 사용하여 로직의 기준을 설정해주었다. 함수를 사용하는 것 대신 접근자 프로퍼티를 쓰게되면 매개변수에 대한 고민이 줄어들어 사용하였다.
    • 하지만 접근자 프로퍼티를 사용하기 위해서는 데이터를 객체안에서 보관해야 하고 그에 따라 접근 방식이 마침표 표기법과 대괄호 표기법으로 depth가 길어져 가독성이 떨어지는 것을 느꼈고 굳이 state로 관리해줄 필요가 없다고 판단하여 state를 제거하여 변수로 나타내고 접근자 프로퍼티는 함수로 나타내었다.

3. Acordion

  • $container = document.querySelector(’.acordian1’) 이다. querySelector가 꼭 document, window에만 붙을 수 있는 것이 아니라 node에 사용 가능한 메서드이다.
1
2
3
4
5
6
7
8
9
10
11
12
// 리팩터링 전
const getHeight = (e) => {
const $item = $container.querySelector(".active > ul > li");
const height = getComputedStyle($item).getPropertyValue("height");
const count = e.target.closest(".active").querySelector("ul > li").length;

return +height.replace("px", "") * count + "px";
};

// 리팩터링 후
const getHeight = ($article = $container.querySelector("article")) =>
$article.querySelector("ul").scrollHeight + "px";
  • ul 요소의 scrollHeight 값을 구하면 overflow : hidden 처리된 요소까지 포함한 높이를 구할 수 있으므로 간결하게 사용할 수 있다.

오늘 배운 점

  1. 접근자 프로퍼티를 사용하여 객체의 응집도를 높이고 가독성을 키울 수 있다는 것을 배웠다.
  2. Array.fill() vs Array.from() 의 차이를 알게 되었다.
1
2
3
4
5
6
arr.fill(value[, start[, end]])

// EX)
[1, 2, 3].fill(4); // [4, 4, 4]
[1, 2, 3].fill(4, 1); // [1, 4, 4]
[1, 2, 3].fill(4, 1, 2); // [1, 4, 3]
  • Array.fill()은 ()안의 요소를 배열에 시작부터 끝 인덱스까지 채운다.
1
2
3
4
5
6
Array.from(arrayLike[, mapFn[, thisArg]])

mapFn : 배열의 모든 요소에 대해 호출할 맵핑 함수

// EX)
Array.from([1, 2, 3], x => x + x); // [2, 4, 6]
  • Array.from()은 배열로 변환하고자 하는 유사배열 객체 또는 Iterable 객체를 얕게 복사하여 새로운 Array 객체를 만든다.
  • 주로 이중배열을 작성할 때 사용한다.

댓글 공유

  • page 1 of 1

loco9939

author.bio


author.job