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를 가지고 고차함수를 사용하는데 어려움이 있었다.