나는 곧 직장생활을 하게 될 취업준비생이다.
회사는 막연한 이미지만 갖고 있어
내가 회사생활을 잘 해낼 수 있을지 걱정도 들고,
현실은 어떤지에 대한 궁금증이 있었다.
이러한 궁금증들을 이 책을 통해 해소할 수 있을 것 같았다.
그리고 목차를 훑어보는데
“또라이 대처법”
와.. 저자는 도대체 어떤 회사생활을 했던걸까? 하며 빨리 책을 보고 싶었다.
일반적으로 1급 시민의 조건을 다음과 같이 정의한다.
변수(variable)에 담을 수 있다.
인자(parameter)로 전달할 수 있다.
반환값(return value)으로 전달할 수 있다.
예를들어, 대부분의 프로그래밍 언어에서 “숫자”는 1급 시민의 조건을 충족한다.
변수에 저장 가능
인자나 반환값으로 전달 가능
1급 객체(first class object)라는 것은 특정 언어에서 객체(object)를 1급 시민으로써 취급한다는 뜻이다.
1급 객체 뿐만 아니라 1급 함수도 존재한다.
함수를 1급 시민으로 취급하는 것이다.
런타임(runtime) 생성 가능
익명(anonymous)으로 생성 가능
JavaScript에서는 객체를 1급 시민으로 취급한다.
JavaScript의 함수는 객체로써 관리되므로 1급 객체라고 볼 수 있다.
JavaScript의 함수는 1급 함수의 추가조건도 만족한다.
JavaScript는 1급 객체인 동시에 1급 함수이지만, 보통 1급 객체로 기술하는 편인듯하다.
아마 함수가 객체이기 때문이지 않을까 싶다.
함수가 1급 객체라는 사실은 겉으로 봤을 땐 그리 특별하지 않다.
함수를 그냥 주고받을 수 있다는 것 뿐이지만 이것이 아주 큰 차이점을 만든다.
가장 중요한 장점은 바로 고차 함수(high order function)가 가능하다는 점이다.
JavaScript의 each, filter, map, sort 등의 함수들이 얼마나 편리한지는 잘 알고 있을 것이다.
인자로 목적에 맞는 함수를 하나 넘겨주면 아주 쉽게 처리가 된다.
반면, Java 7의 Collections.sort함수같은 경우도 비교를 위해 인자를 하나 넘겨주게 되는데,
이것은 함수가 아니라 Comparator interface 타입의 인스턴스(instance)이다.
함수 하나를 넘겨주기 위해서 새로운 클래스를 만들고 그것의 인스턴스까지 생성해야 하는 것이다.
ES6와 Java 8에서는 람다(lambda)가 지원되면서 훨신 간편해졌다.
1급 객체가 JavaScript의 클로져(closure)와 만나면 또 하나의 장점이 생긴다.
JavaScript의 함수는 생성될 당시의 Lexical Environment를 기억하게 되는데,
함수를 주고받게 되면 이 Lexical Environment도 함께 전달된다.
이것을 이용해서 커링(currying)과 메모이제이션(memoization)이 가능해진다.
이 주제는 기회가 될 때 따로 다룰 예정이다.
https://bestalign.github.io/2015/10/18/first-class-object/
저번 포스팅에 이어서 투두리스트에 변경된 내용이다.
1. 로컬스토리지 적용 (새로고침해도 데이터 그대로 남아있도록)
2. 모듈시스템 적용 (import해서 쓸 수 있도록)
아이템을 추가했을 때와 삭제했을 때,
새로고침시, 데이터가 남아있는 것을 확인할 수 있다.
index.js
(생략)
index.html
(생략)
app.js
(생략)
models.js
(생략)
TodoManagerWithStorage.js
import {TodoManager} from './models.js';
class TodoManagerWithStorage extends TodoManager {
static get STORAGE_KEY() { // [A]
return "TODO-APP";
}
constructor() { // [B]
const todoJSON = localStorage.getItem(TodoManagerWithStorage.STORAGE_KEY);
const todos = (todoJSON) ? JSON.parse(todoJSON) : [];
super(todos)
}
addTodo(contents, done = false) { // [C]
const newTodo = super.addTodo(contents, done);
const original = newTodo.toggle;
newTodo.toggle = () => {
original.apply(newTodo);
this.saveToLocalStorage();
}
this.saveToLocalStorage();
return newTodo;
}
delTodo(index) { // [D]
super.delTodo(index);
this.saveToLocalStorage();
}
saveToLocalStorage() { // [E]
const todoJSON = JSON.stringify(this._todos);
localStorage.setItem(TodoManagerWithStorage.STORAGE_KEY, todoJSON);
}
}
export {TodoManagerWithStorage};
[A] 저장소 키를 get타입의 정적메소드로 정의한다.
(로컬스토리지에 이 키를 사용해 데이터를 set하고 get하기 위함)
[B] 생성자를 정의한다. (TodoManagerWithStorage클래스)
로컬스토리지의 getItem()메소드를 통해 해당 저장소 키에 대한 데이터를 가져온다.
가져온 데이터는 JSON문자열로 파싱한다. (데이터가 없을 경우 빈리스트 할당)
JSON문자열을 super클래스의 생성자에 전달한다.
(이 때, TodoManager의 생성자는 데이터들을 하나하나 push하여 리스트를 생성한다)
[C] TodoManager의 addTodo 메소드를 오버라이딩 한다. (메소드 재정의)
먼저, 기존의 addTodo 메소드를 호출하고, toggle 메소드를 변수에 할당한다.
할당한 toggle 메소드를 재정의한다.
변수에 할당한 tooggle 메소드를 apply()메소드를 통해 newTodo를 this객체로 호출한다.
saveToLocalStorage()메소드를 호출한다.
(완료 여부(done)가 변경될 때 마다 로컬스토리지에 동기화한다)
[D] 기존의 delTodo 메소드를 호출하고, saveToLocalStorage()메소드를
호출하여 삭제된 것을 로컬스토리지에 동기화한다.
[E] 할 일 리스트를 json문자열로 바꿔서 로컬스토리지의 setItem을 통해 데이터를 저장한다.
전체 소스코드 링크 : https://github.com/teddy8/vanilla-js-todolist
JS에서 call(), apply(), bind() 메소드를 예제를 통해 정리한다.
const test1 = {
num: 4,
sum(a, b) {
return this.num + a + b;
}
}
const test2 = {
num: 2
}
num = 3;
위와 같은 소스코드가 미리 작성되어 있을 때, 각 메소드에 따른 사용방법을 살펴본다.
console.log(test1.sum.call(test2, 2, 3)); // 7
console.log(test1.sum.call(test1, 2, 3)); // 9
call()메소드는 첫 번째 인자에 this를 전달한다.
두 번째 인자부터는 call()을 수행하는 메소드에 해당하는 인자값을 넣어준다.
변수 test의 sum(a, b)메소드는 전달받은 2개의 숫자와 this의 num값과 더한 값을 반환한다.
첫 번째 출력결과는
test2의 this를 전달했기 때문에 해당 this의 num값인 2와 2, 3을 더 한 값인 7이 출력된다.
두 번째 출력결과는
test1의 this를 전달했기 때문에 해당 this의 num값인 4와 2, 3을 더 한 값인 9가 출력된다.
console.log(test1.sum.apply(test2, [2, 3])); // 7
console.log(test1.sum.apply(test1, [2, 3])); // 9
call()메소드와의 차이는 apply()메소드는 인자를 묶어서 전달한다는 점이다.
const a = test1.sum;
console.log(a(2, 3)); // 8
const newA = a.bind(test1);
console.log(newA(2, 3)); // 9
첫 번째 출력결과는
test1의 sum()메소드를 다른 변수에 저장하고 그 변수를 통해 메소드를 호출하면 this는 전역객체를 가리킨다.
따라서, 전역객체의 num값인 3과 2, 3을 더 한 8이 출력된다.
두 번째 출력결과는
test1의 sum()메소드를 bind()메소드를 통해 test1을 기준으로 this를 지정했다.
따라서, 해당 this의 num값인 4와 2, 3을 더 한 값인 9가 출력된다.
call(), apply(), bind()메소드는 원하는 환경의 this를 지정하여 사용할 수 있다는 점이 동일하다.
저번 포스팅에 이어서 투두리스트에 삭제기능을 추가한 것을 정리한다.
각 아이템의 오른쪽에 있는 'X'버튼을 통해 메모를 제거할 수 있다.
example.html
<!DOCTYPE html>
<html>
<head>
<title>할 일 앱 만들기 예제</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="title">
<h1>나의 하루</h1>
<h2>10월 28일</h2>
</div>
<div class="todo-container">
</div>
<div class="add-todo">
<button>+</button>
<input type="text" placeholder="할 일 추가">
</div>
</body>
</html>
index.js
import './style.css';
import {Todo, TodoManager} from './models.js';
class TodoApp {
constructor(todos) {
this.todoManager = new TodoManager(todos);
this.todoContainerEl = document.querySelector(".todo-container");
this.titleEl = document.querySelector(".title h2");
this.plusBtnEl = document.querySelector(".add-todo button");
this.renderTodos();
this.bindEvents();
}
renderTodos() { // 할 일 데이터를 화면에 그림
const todoCount = document.getElementsByClassName('todo').length;
this.todoManager.getList().slice(todoCount).forEach((todo, i) => {
const addIndex = todoCount + i;
const todoEl = this.createTodoEl(todo, addIndex);
this.todoContainerEl.appendChild(todoEl);
this.todoManager.getList()[addIndex].id = addIndex; // 고유키 할당
});
this.renderTitle();
}
createTodoEl(todo, id) { // '+'버튼을 눌렀을 때 생성 될 div 영역.
const todoEl = document.createElement("div");
todoEl.dataset.todoId = id;
todoEl.classList.add("todo");
todoEl.innerHTML =
`<input type="checkbox" ${todo.done ? "checked" : ""}>
<label>${todo.contents}</label>
<span class="close">×</span>`;
todoEl.addEventListener('click', evt => {
if(evt.target.type == 'checkbox') {
const index = todoEl.dataset.todoId;
this.todoManager.getList()[index].toggle();
this.renderTitle();
}
if(evt.target.className == 'close') { // [A]
const index = todoEl.dataset.todoId;
todoEl.remove();
this.todoManager.delTodo(index);
this.renderTitle();
}
})
return todoEl;
}
renderTitle() { // 현재날짜와 남은 할 일 갱신
const now = new Date();
const month = now.getMonth() + 1;
const date = now.getDate();
if (this.titleEl) {
this.titleEl.innerHTML =
`${month}월 ${date}일 <span class="left-count">
(${this.todoManager.leftTodoCount}개)</span>`;
}
}
bindEvents() { // 이벤트에 반응하는 리스너 함수를 등록하는 메소드
this.plusBtnEl.addEventListener('click', evt => {
const textEl = document.querySelector('.add-todo input[type="text"]');
this.todoManager.addTodo(textEl.value);
textEl.value = '';
this.renderTodos();
});
}
}
const todoApp = new TodoApp([
{ contents: "공부하기", done: false },
{ contents: "놀기", done: true },
{ contents: "밥먹기", done: false }
]);
models.js
class Todo {
constructor(contents, done) {
this.contents = contents;
this.done = done;
}
toggle() {
this.done = !this.done;
}
}
class TodoManager {
constructor(todos = []) {
this._todos = [];
todos.forEach(todo => {
this.addTodo(todo.contents, todo.done);
});
}
addTodo(contents, done = false) {
const newTodo = new Todo(contents, done);
this._todos.push(newTodo);
return newTodo;
}
delTodo(index) { // [A]
this._todos = this._todos.filter(function(obj) {
return obj.id != index;
});
}
getList() {
return this._todos;
}
get leftTodoCount() {
return this._todos.reduce((p, c) => {
if (c.done === false) {
return ++p;
} else {
return p;
}
}, 0);
}
}
export {Todo, TodoManager};
삭제 기능의 핵심부분은 index.js와 models.js에 [A]로 주석친 곳이다.
먼저, remove()를 사용해 화면에 보이는 아이템을 제거해준다.
그 후, 모델클래스로 생성한 인스턴스에 동기화시켜주기 위해
dataset의 todoId값을 가져와 삭제할 아이템의 고유키값을 delTodo()메소드로 전달한다.
해당 키값을 갖지 않는 아이템을 filter해준다.
즉, 해당 키값을 갖는 아이템은 제거된다.