HolyMoly 프로젝트

오늘부터 약 2주간 자바스크립트 프로젝트가 시작된다.

그동안 페어프로그래밍을 통해 배운 내용을 실전 프로젝트에 적용해보고, 게임까지 직접 만들어보면서 재미까지 더불어 챙길 수 있는 프로젝트이다.

주요 기능은 로그인과 회원가입 기능을 구현해야하며, 라우터 기능을 구현해야한다.

우리팀은 3명의 팀원으로 구성되어있고 각자 1개의 미니게임을 구현하고 싶어한다.

메인페이지

메인 페이지에는 캐러셀 기능으로 게임 설명을 보여줄 것이다. 캐러셀 기능은 페어프로그래밍 1 때 기능 구현을 경험해본 바 있다.

그 때, transitionend가 애먹었던 기억이 아직도 잊혀지지 않는다.

로그인 및 회원가입 페이지

주요 기능인 로그인 및 회원가입 페이지 구현은 페어프로그래밍 1에서 구현해보았다. 하지만 시간이 부족하여 제대로 리팩터링을 경험하지 못한체로 끝냈었다.

이번 기회에 그 때 하지 못한 리팩터링까지 같이 해볼 수 있는 기회가 생긴 것 같다.

미니게임 구현

내가 구현할 미니게임의 제목은 “Close to Zero” 게임이다.

말 그대로 0에 가장 근접할 때 버튼을 클릭해서 그 숫자가 가장 0에 근접한 사람이 승리하는 게임이다.

게임 구현은 간단할 것 같다. 하지만 이번 프로젝트는 지난 페어프로그래밍 2편 9번 문제에서 CBD Library를 제작한 것을 활용하여 구조를 가져갈 것이기 때문에 컴포넌트에 대한 이해가 필요하다.

라우터 기능 구현

이번 프로젝트는 앞서 말했듯이 컴포넌트 단위로 HTML 동적으로 생성하는 SPA를 구현할 것이다. SPA는 화면이 전환되어도 URL이 변경되지 않아 SEO에 취약하다는 문제점이 있다.

또한, 이번 프로젝트의 최소 요구사항인 라우터 기능을 구현하기 위해서는 URL이 필요한데, 과연 URL이 없는 SPA에서 어떻게 라우터를 구현할 수 있을까?

그 해답은 컴포넌트와 라우터를 1:1 매핑관계로 두어 URL 뒤의 Path가 변경되면 컴포넌트를 바꿔주면 된다.

이에 대한 자세한 방법은 다음 시간에 알아보자.

소감

팀원들과 같이 줌으로 회의를 하면서 아이디어도 잘나오고 오늘 회의 열심히 달려서 figma 시안도 완성하여서 뿌듯한 공휴일을 보낸 것 같아 기분이 좋다.

내일부터 다시 또 피터지는 프로젝트 시간이 시작되니 컨디션 조절 잘하고 매일 회고도 잊지 않고 지치지 않고 롱런하고 싶다.

댓글 공유

type 체크 문제 Tip

카테고리 Algorithm

문제

입력값에 {}, [], null이 들어가면 출력값으로 type을 반환하도록 하는 함수를 만들어라

내 코드

1
2
3
4
5
6
7
8
9
10
11
const typeIs = (data) => {
if (typeof data !== "object") {
throw new TypeError(`${data}는 'object' 타입이 아닙니다.`);
}
if (Array.isArray(data)) {
return "array";
} else if (data === null) {
return "null";
}
return "object";
};
  • 입력값에 object 타입만 받을 수 있도록 에러처리를 해주었다.
  • if, else if 문으로 조건에 따라 반환값을 반환해주었다.

새로운 코드

1
2
3
4
const typeIs = data => {
return ({}).toString.call(data).toLowerCase().slice(8, -1);
}
toString() 메서드는 “[object type]”을 반환한다.
  • call 메서드를 사용하여 data 값을 바인딩 toString() 메서드에 바인딩 해주었다.

댓글 공유

boj-2525 오븐시계

카테고리 Algorithm, boj

boj 2525 오븐시계

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const fs = require("fs");
const input = fs.readFileSync("dev/stdin").toString().trim().split("\n");

let time = input[0].split(" ");
let oven = +input[1];

let hour = +time[0];
let minute = +time[1] + oven;

if (minute >= 60) {
hour += Math.floor(minute / 60);
minute %= 60;
hour = hour >= 24 ? hour - 24 : hour;
}

console.log(hour + " " + minute);

맞는데 왜 틀리지 를 10번 넘게 한 문제이다.

핵심은 oven 시간이 148분인 경우이다. 이 경우 시간이 2시간이 추가 되어야 하고 148 을 60으로 나눈 나머지를 minute에다가 더해주면 된다.

나는 1시간만 추가된 경우만 고려해줘서 틀렸다.

댓글 공유

페어프로그래밍 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에 이전되는 문제가 발생하였는데 이는 따로 정리해둬야겠다.

댓글 공유

loco9939

author.bio


author.job