[초보자들] S70 스터디 여섯번째 후기(마지막)


자바스크립트 함수와 객체에 대해 알아가보는 S70 강의가 끝났습니다ㅠㅠ 지난 강의에서 역할에 맞게 객체를 분리했습니다. 그리고 객체가 다형성을 충족하기 위해 리팩토링을 했죠. 이로써 모든 역할이 잘 분리되었을까요? 마지막 강의를 통해 더 세분화 시킬 역할이 없는지 알아보고, To-Do List를 마무리 지어보겠습니다^^

역할 분리 1top

우리는 To-Do List 프로그램 코드를 계속 수정했습니다. 이곳 저곳 엄청 많이 변했죠. 그 중 중요한 점은 역할을 분리해 나가는 과정일 것입니다.
역할은 왜 분리해야 할까요? 분리하지 않고 코드를 쭉~~~~~~ 작성해버리면 안되나요?
6주 동안의 스터디에서도 이렇게 많은 코드가 변했는데.. 아마 다른 프로그램은 이보다 더 많이 수정될 것 입니다. 코드의 수정은 그에 따른 여파가 생깁니다. 하지만 우리는 길게 늘어져있는 코드에서 그 여파가 어떻게 발생할지 완벽히 알긴 어렵습니다.
그래서 역할을 분리합니다. 모든 것이 역할에 따라 분리되어 있다면, 수정으로 인한 여파는 한정적일 것이고, 그럼 유지보수를 좀 더 쉽게 할 수 있을테니까요.

그럼 우리는 이제 역할을 좀 더 분리해보겠습니다. 첫 번째로 todo의 addTask를 보죠.

var todo = (function(){
    var tasks = [];
    var addTask = (function(){
        var id = 0;
        return function(title){
            var result = id;
            tasks.push({id: id++, title: title, state: STATE.PROGRESS()});
            render();
            return result;
        }
    })();  
    ...
})();

tasks에 들어가는 각각의 ‘할 일’은 title, id, state라는 값을 가지고 있는 단순 객체입니다.
그렇다보니 task들은 자신의 상태가 진행인지 완료인지 스스로 평가할 수 없고, 자신의 role이 무엇인지 얘기할 수 없어서 다른 애들(todo, html, con)이 id라는 값으로 존재를 식별합니다.

불쌍한 task.. Task 형을 만들어 주도록 하겠습니다.

지금의 ‘할 일’ 객체는 id라는 값으로 존재를 판단하고 있습니다. 이게 바른 방법일까요?
지난 시간 javascript의 데이터 타입을 기본형과 참조형으로 나눠 그 둘의 비교 방법 차이에 대해 언급했습니다. <click! 지난 강의 후기보기>
기본형은 값으로 판단하고, 참조형은 식별로 판단 한다고 했었죠. ‘할 일’은 객체. 즉 참조형입니다. 그렇다면 id라는 값으로 어떤 객체인지를 판단하는 것이 아닌 식별로 판단하는 것이 바른 방법일 것입니다. 따라서 더 이상 id라는 값은 필요가 없다는 것이죠.

그렇다면, Task 형은 title, state라는 고유한 상태(속성)에 의존하는 객체입니다.

var Task = function(title){
    //개발자들 사이에서 관용적으로 접근하지 말라는 뜻으로 _를 붙여요~
    this._title = title;
    this._state = STATE.PROGRESS();
}

state에는 p, c라는 두 가지 값이 있습니다. 이 값은 원래 STATE라는 객체로 분리했지만, 이는 더 이상 밖으로 공유될 것이 아닌 Task만 알면되므로 즉시실행함수로 감싸 만들어진 스코프에 은닉시키도록 하겠습니다.

var Task = (function(){
    var p = {}, c = {};
    var Task = function(title){
        this._title = title;
        this._state = p;
    };
    return Task;
})();

이 때 안쪽 스코프에서 사용하는 변수 이름과 바깥쪽에서 사용할 변수 이름을 똑같이 합니다. 그러면 스코프 안 쪽에서는 스코프 바깥쪽에 있는 변수를 알 수 없게 됩니다. 이 기법을 가린다는 의미로 쉐도잉이라고 합니다. 쉐도잉을 통해서 ‘내부 Task가 나중에 외부 Task가 될거야~’라는 의미를 가지게 되죠. 이제 앞으로의 객체는 쉐도잉 기법을 사용해 만들도록 하겠습니다.
저번 시간 객체를 만들어가며, 함수 스코프 대신 객체 스코프를 쓴다고 했었습니다. 그래서 의존하는 값을 this에 넣었구요.
하지만 그렇다고 해서 함수 스코프를 쓰지 않는게 아니네요. class를 정의하는 함수도 어차피 스코프의 영향을 받기 때문에 함수 스코프를 이용하기도 합니다.
여기까지 Task 형을 만들었습니다. 이제는 아이덴티티 메서드를 정의해 보겠습니다.
Task 형은 바깥쪽과 어떤 대화를 할 수 있어야할까요?

할 일의 상태가 궁금하다!

바깥에서는 Task의 state가 진행인지 완료인지 궁금하지 않을까요? 그래서 state 값을 직접 알려주지 않고, Task가 state를 직접 평가하고 isComplete라는 메서드를 통해 평가 결과만 boolean 값으로 알려주도록 하겠습니다.

var Task = (function(){
    var p = {}, c = {};
    var Task = function(title){ ... };
    Task.prototype.isComplete = function(){
        return this._state === c;
    };
    return Task;
})();

할 일의 상태를 바꾸고 싶어!

todo의 toggle 함수는 Task의 state를 진행이면 완료로 바꾸고 완료라면 진행으로 바꾸었습니다. Task의 내장 state를 꺼내서 막 바꿨었죠. Task의 내장을!!!!

마음대로 보여주고 싶지 않았는데 마음대로 값을 바꾸도록 한다는 것은 말이 안되죠.
그래서 외부에서 원한다면 Task가 직접 바꿀 수 있도록 toggle이라는 메서드를 만들도록 하겠습니다.

var Task = (function(){
    var p = {}, c = {};
    var Task = function(title){ ... }
    Task.prototype.isComplete = function(){ ... };
    Task.prototype.toggle = function(){
        if(this._state === c) this._state = p;
        else this._state = c;
    };
    return Task;
})();

할 일이 뭐야???

Renderer를 통해서 화면을 그려나갔습니다. 화면을 그릴 때는 Task의 title이 필요했죠. 그런데 바깥에서 굳이 ‘할 일의 title 값이 뭐야??’ 라고 물어봐야할까요? 그냥 ‘할 일이 뭐야?’라고 물어보면 알려줄 수 있으면 되는거죠. 굳이 Task 안의 title이라는 값을 알 필요 없게..
그래서 Task 객체를 문자열로 읽을 때 객체의 내부 함수인 toString이 호출 됩니다. 이를 override해서 title 값을 알려주도록 하겠습니다.

var Task = (function(){
    var p = {}, c = {};
    var Task = function(title){ ... }
    Task.prototype.isComplete = function(){ ... };
    Task.prototype.toggle = function(){ ... };
    Task.prototype.toString = function(){
        return this._title;
    };
    return Task;
})();

이로써 Task 형이 만들어 졌습니다. title, state라는 상태는 은닉시키고 isComplete, toggle, toString 메서드로 고유한 상태에 대한 정보를 캡슐화해서 제공할 수 있게되었습니다.

Refactoringtop


기존에 Todo, Html, Con에는 더 이상 Task를 직접 건드릴 수 없게되었습니다. 이에 맞게 Todo, Html, Con의 코드를 고쳐보도록 하겠습니다.

todo 객체

<click! 기존 todo 코드 보러가기>
Todo 객체에서 ‘할 일’을 직접 건드리던 함수는 addTask, removeTask, toggle, changState가 있습니다. ‘할 일’의 state 값을 바꾸는 changeState 함수는 없애고, ‘할 일’에 관한 일 모두 Task에 맡기도록 코드를 변경해 보죠.

var todo = (function(){
    var tasks = [];
    var addTask = function(title){
        
        //(기존코드) tasks.push({id: id++, title: title, state: STATE.PROGRESS()});
        // Task 인스턴스를 생성해 tasks에 추가한다. 
        tasks.push(new Task(title));

        render();
    };
    var removeTask = function(task){
        
        //(기존코드) if(tasks[i].id === id)
        // 삭제할 task 객체를 식별한다.
        if((task instanceof Task) && tasks.indexOf(task) > -1){
            tasks.splice(tasks.indexOf(task), 1);
        }

        render();
    };
    var target;
    var render = function(){ ... };
    return {
        setRenderer: function(renderer){ … }.
        add: addTask,
        remove: removeTask,
        toggle: function(task){

            //(기존코드) if(tasks[i].id === id)
            // toggle할 task 객체를 식별한다. 
            if((task instanceof Task) && tasks.indexOf(task) > -1){
                task.toggle();
            }

        }
    };
})();

Html, Con형

<click! 기존 html, con 코드 보러가기>
기존의 HTML 문서에 출력하는 렌더링을 담당하는 Html 형과 console 에 출력하는 렌더링을 담당하는 Con 형에서 할 일의 상태를 판단하기 위해 state의 값을 직접 확인합니다. Task의 내장을 꺼내서… 확인하다니 ㅠㅠ
Task 객체가 만들어져서 이제 많은 것들이 변해야합니다. 할 일에 관한 많은 부분을 Task 에게 맡기도록 Html 형 코드를 고쳐보도록 하죠.

var Html = function(){};
Html.prototype = new Renderer();
Html.prototype._init = function(){ ... };
Html.prototype._render = function(){
    ...
    var task;
    for(var i = 0; i < this.tasks.length; i++){
        task = this.tasks[i];
        
        //(기존코드) if(task.state === STATE.PROGRESS())
        // task의 inComplete 메서드로 상태를 판단한다.
        if(task.isComplete()){ 
            child = this.progressLi.cloneNode(true);
           
            //(기존코드) child.querySelector('p').innerHTML = task.title;
            // task의 toString 메서드로 title 값을 출력한다.
            child.querySelector('p').innerHTML = task;
            
            inputs = child.querySelectorAll('input');
            inputs[0].onclick = function(){
                
                //(기존코드) this.todo.toggle(this.getAttribute('data-task-id'));
                // task 객체로 어떤 것의 상태를 변경할지 식별한다.
                this.todo.toggle(task);

            };
            inputs[1].onclick = function(){
                
                //(기존코드) this.todo.remove(this.getAttribute('data-task-id'));
                // task 객체로 어떤 것을 삭제할지 식별한다.
                this.todo.remove(task);

            };
            progress.appendChild(child);
        }else{
            child = this.completeLi.cloneNode(true);

            child.querySelector('p').innerHTML = task;
            inputs = child.querySelectorAll('input');
            inputs[0].onclick = function(){
                this.todo.toggle(task);
            };
            inputs[1].onclick = function(){
                this.todo.toggle(task);
            };
            complete.appendChild(child);
        }
    }
    ...
};

이로써 Task 형 분리로 인한 코드 수정은 완료되었습니다. 하지만 사실 이 코드에는 또 다른 문제가 있습니다. 추후에는 역할을 더 분리하면서 Html 형 코드가 바뀌게 되어 문제가 해결되지만, 지금 상태에서 무엇이 문제인지 이를 어떻게 해결할 수 있을지 살펴보기로 하겠습니다.

_render 메서드에서 for 문으로 tasks에 있는 모든 할 일 항목을 만들어 갑니다. 그래서 각 할 일 항목을 만들 때 this.todo.toggle 메서드와 this.todo.remove 메서드에 인자에 해당 할 일 객체(tasks[0], tasks[1]…)을 전달하려고 하죠. 그래서 task라는 변수에 해당 할 일 객체(tasks[0], tasks[1]…)를 할당해 전달합니다.

하지만 이 때 우리의 예상과는 다르게 값이 전달됩니다.
예를 들어 tasks에는 총 3개의 할 일이 들어있다고 가정하겠습니다. 우리의 예상된 시나리오는 이러합니다.
첫 번째 loop에서 tasks[0]을 가리키는 task가 전달됩니다.
두 번째 loop에서 tasks[1]을 가리키는 task가 전달됩니다.
세 번째 loop에서 tasks[2]를 가리키는 task가 전달됩니다.

task 변수는 loop를 돌아갈 당시에는 각 할 일 객체(tasks[0], tasks[1], tasks[2])를 가리키고 있을 것 입니다.
하지만 task 변수는 _render 메서드 안에서 선언된 지역변수 입니다. 그래서 loop를 돌 때 task가 가리키는 값이 변동되긴 하지만 결국 같은 스코프(_render 메서드 내부 스코프)의 하나의 변수입니다.
따라서 첫 번째, 두 번째, 세 번째 할 일 모두 같은 task를 가리키고 있고, 이 들이 추후에 실행될 때는 마지막으로 가리킨 tasks[2]가 전달된 셈인거죠.

그렇다면 이를 어떻게 해결할 수 있을까요?
위에서 언급한대로 문제는 task가 같은 스코프의 하나의 변수라는 것입니다. 따라서 각 loop마다 새 스코프의 task를 가리키도록 만들어 해결할 수 있습니다. 그래서 for문 안에서 즉시실행함수로 새 스코프를 만들어주겠습니다.

이 때 주의할 점! 함수 호출 패턴에 따른 this 바인딩입니다.
이 내용은 강의 교재 인사이드 자바스크립트의 ‘chapter 4.4 함수 호출과 this’를 참고합니다.
메서드(객체의 속성)일 때의 this는 메서드를 호출하는 객체에 바인딩되고, 객체의 속성이 아닌 함수일 때 this는 전역객체(window객체)에 바인딩됩니다.
따라서 즉시실행함수 내부에서 this를 사용하면 이는 객체가 아닌 전역 객체를 가리키게 됩니다. 그래서 접근가능한 self 변수에 this를 할당해 놓고 즉시실행함수 내부에서는 this대신에 self을 쓰도록 하겠습니다.

Html.prototype._render = function(){
    ...
    // self변수에 this를 할당합니다.
    var self= this;

    for(var i = 0; i < this.tasks.length; i++){
        
        // 새 스코프 생성
        (function(){
            var task = self.tasks[i];
            ...
            inputs[0].onclick = function(){
                self.todo.toggle(task);
            };
            ...
        })();
    }
    ...
};

Contexttop

우리는 캡슐화, 다형성, 상속을 이용하여 코드 재사용을 증가시키고, 유지 보수를 위해 객체 지향 프로그래밍을 하고 있습니다.
객체 지향 프로그래밍에서 객체란?! 4강에서 언급했다시피 고유한 상태가 있고, 그 상태를 이용하는 메서드로 이루어진 것을 말합니다. 그런데 객체 중에는 공통된 역할을 가진 객체들이 있습니다. 그래서 공통된 역할을 가진 객체에 대해 ‘이 역할을 가진 객체는 이런거야~’라는 청사진을 만들어 놓습니다. 청사진으로 부터 실체화된 객체를 여러 개 만들 수 있습니다.

‘청사진만 있으면 여러 개 만들 수 있어~’

이런 청사진이 역할에 대한 프로토콜이라고 하고 인터페이스, 클래스라고도 하죠. 그리고 이 청사진으로 부터 실체화된 객체를 인스턴스라고 합니다.

예를 들어 배우 A, B가 있습니다. 이 들은 배우라는 역할로 감정이라는 상태와 연기한다라는 메서드를 가지고 있습니다.
여기서 배우라는 역할의 청사진이 Actor클래스가 됩니다. 즉 Actor클래스는 감정이라는 상태가 있고 감정을 연기한다는 메서드가 있다는 것을 약속합니다. 그리고 A, B가 Actor클래스로 부터 실체화된 인스턴스라고 할 수 있습니다.

그런데 A, B는 Actor클래스로 감정이라는 고유한 상태를 가지고 있다는 것은 맞지만, 실제로 그 값은 다르지 않을까요?
감정이라는 상태가 A는 ‘기쁘다’라는 값이고, B는 ‘슬프다’라는 값인 것처럼 각자 가지고 있는 상태의 실제 값은 다른거죠. 인스턴스마다 가지고 있는 각자의 환경이 바로 context입니다.
지난 강의시간의 Refactoring에서 만들어진 객체들에는 this라는 것을 사용했습니다. 이를 클래스로 부터 파생된 인스턴스를 가리킨다라고 얘기했었죠. 사실 이 this가 가리키는 것은 각 인스턴스 마다 가지고 있는 context를 의미합니다.
그럼 이 개념을 가지고 Actor클래스를 만들어보도록 하겠습니다.

var Actor = function(){};
Actor.prototype.init = function(name){
    this.name = name;
};
Actor.prototype.express = function(){
    if(!this.emotion) return;
    console.log(this.name + '는 ' + this.emotion);
};
Actor.prototype.setEmotion = function(emotion){
    this.emotion = emotion;
};
var A = new Actor();
var B = new Actor();

A.init('김에이');
B.init('박비이');
A.setEmotion('기쁘다');
B.setEmotion('졸리다');
A.express();
B.express();

하나의 클래스에서 두 개의 인스턴스를 만들었습니다. 그리고 이 클래스의 메서드는 context가 어떻든 간에 똑같은 처리를 하고 있습니다. 즉 메서드는 context에 제네릭화 되어있는 함수라고 할 수 있습니다. 이 개념을 이용해 우리가 만들고 있는 To-Do List 프로그램을 살펴보도록 하겠습니다.

TaskManagertop

To-Do List 앱을 써보신 적 있나요? 이런 저런 기능이 굉장히 많습니다. 그리고 특히 리스트가 하나가 아닌 여러개를 만들 수 있죠! 그런데 지금 todo는 하나의 객체로 되어 있습니다. 따라서 여러 개를 만들 수 없습니다. 그래서 todo를 클래스로 바꿔보겠습니다. 그래야 new 키워드로 여러 개 만들죠.
우리가 편의상 todo라고 이름지었지만 진짜로 하는 일이 뭘까요? 일단 Task를 여러개 소유하고 있습니다. 이를 어그리게이션(aggregation)하고 있다고 표현합니다. 어떤 객체가 다른 객체를 소유할 때 집합으로 소유하는 행위를 말하죠.
다시 말해 todo는 task를 어그리게이션하고 이를 관리 관리하는 역할을 합니다. 그래서 역할에 맞는 이름인 TaskManager로 바꾸고 형을 만들어 보겠습니다.
TaskManager 형은 tasks와 renderer에 의존하고 내부에서 쓰는 render 함수와 setRenderer, add, remove, toggle 메서드가 있는 객체입니다.

var TaskManager = (function(){
    var TaskManager = function(){
        this._tasks = [];
        this._renderer = null;
    };
    TaskManager.prototype._render = function(){};
    TaskManager.prototype.setRenderer = function(){};
    TaskManager.prototype.add = function(){};
    TaskManager.prototype.remove = function(){};
    TaskManager.prototype.toggle = function(){};
    
    return TaskManager;
})();

‘클래스이름.prototype’이 너무 길어요ㅠㅠ 그래서 표준적으로 이를 fn 이라는 변수에 할당에서 사용합니다. 그리고 나머지 코드들도 채워 나가보도록 하죠.

var TaskManager = (function(){
    var TaskManager = function(){ ... };
    
    // fn 변수에 TaskManager.prototype 할당!
    var fn = TaskManager.prototype;

    fn._render = function(){ this._renderer.render(this._tasks.slice(0)); };
    fn.setRenderer = function(renderer){
        if(!(renderer instanceof Renderer)) return;
        this._renderer = renderer;
        renderer.init(this);
    };
    fn.add = function(title){
        this._tasks.push(new Task(title));
        this._render();
    };
    fn.remove = function(task){
        var tasks = this._tasks;
        if((task instanceof Task) && (this._tasks.indexOf(task) > -1)) tasks.splice(indexOf(task), 1);
        this._render();
    };
    fn.toggle = function(task){
        if((task instanceof Task) && (this._tasks.indexOf(task) > -1)) task.toggle();
        this._render();
    };
    return TaskManager;
})();

remove, toggle 메서드에 중복되는 알고리즘이 있습니다.

if((task instanceof Task) && (this._tasks.indexOf(task) > -1)) 

입력받은 task의 형을 확인하고 tasks에 있는 task인지를 확인하죠. 이런 중복은 함수로 제거해야겠습니다. _checkTask 함수를 만들어 놓고, remove, toggle 메서드에서는 이 함수를 사용하도록 합니다.

fn._checkTask = function(task){
    return (task instanceof Task) && (this._tasks.indexOf(task) > -1);
};
...
fn.remove = function(task){
    var tasks = this._tasks;
    if(this._checkTask(task)) tasks.splice(indexOf(task), 1);
    this._render();
};
fn.toggle = function(task){
    if(this._checkTask(task)) task.toggle();
    this._render();
};
...

익명 함수 공간으로 만들어진 스코프를 가진 기존 todo에서 이제는 class로 암묵적인 context인 this를 사용하는 TaskManager가 되었습니다.
이렇게 context에 의존하도록 만들면 여러개의 인스턴스를 만들 수 있습니다. 즉 이제는 분더리스트(Wunderlist)처럼 폴더 별로 리스트를 여러개 만들 수 있게 되었습니다.

추상화 인터페이스 강제top

Task가 태어나서 굉장히 여러가지가 일어나고 있습니다. html, con에서도 Task에 관련된 코드를 바꿔줘야합니다. 그전에 그들의 부모인 Renderer 형에는 아무 문제가 없는지 확인해 보도록 하겠습니다.

var Renderer = function(){};
Renderer.prototype.init = function(taskManager){
    this.taskManager = taskManager;
    this._init();
}
Renderer.prototype.render = function(tasks){
    this.tasks = tasks;
    this._render();
}
Renderer.prototype._init = function(){};
Renderer.prototype._render = function(){};

일단 관용적으로 밖으로 노출하지 않는 값에는 접근하지 말라는 뜻으로 _를 붙인다고 했었습니다. 그래서 this.taskManager와 this.tasks에는 _를 붙여야겠네요.
그리고 Renderer 형은 자식들이 _init, _render를 override를 하길 원합니다. 그런데 만약에 부모의 말을 듣지 않고 override 하지 않는다면? Renderer 형의 _init, _render를 실행시켜 문제가 발생시키지 않도록 그냥 넘어가야하나요? ‘안돼~ 안돼~’
그래서 추상화된 인터페이스인 _init, _render 메서드를 정의하도록 강제 해보겠습니다. 강제하는 방법은 Renderer의 _init, _render에 throw를 거는 것입니다. <click! throw 란? >

var Renderer = function(){};
Renderer.prototype.init = function(taskManager){
    this._taskManager = taskManager;
    this._init();
}
Renderer.prototype.render = function(tasks){
    this._tasks = tasks;
    this._render();
}
Renderer.prototype._init = function(){
    throw '_init must be override';
};
Renderer.prototype._render = function(){
    throw '_render must be override';
};

이제 만약 Renderer 형의 _init, _render가 호출되면 throw되어 프로그램이 죽어버립니다. 이때 throw 뒤에 메시지를 잘 작성해 프로그램이 죽어버린 이유를 잘 알려주도록 합니다.

역할 분리 2top

Html 형도 위에서 언급한 쉐도잉과 fn을 사용한 패턴으로 고쳐보겠습니다.

var Html = (function(){
    var Html = function(){};
    var fn = Html.prototype = new Renderer();
    fn._init = function(){
        if(typeof this.completeLi !== 'undefined' && typeof this.progressLi !== 'undefined') return;
        this.progressLi = document.querySelector('#todo .progress li');
        this.completeLi = document.querySelector('#todo .complete li');
 
        this.progressLi.parentNode.removeChild(this.progressLi);
        this.completeLi.parentNode.removeChild(this.completeLi);
 
        console.log('task 입력을 처리할 코드');
    };
    fn._render = function(){ … };
 
    return Html;
})();

_init을 override하는 하는 코드를 보겠습니다. 여기서 progressLi하고 completeLi를 document에서 가져오고 있습니다. 또한 parentNode로부터 빼내고 있죠. 여기서 문제가 있습니다.

Html 형은 사실 class가 아니다?!!

_init의 코드는 지역변수, 인자, this도 아닌 것에 의존을 하고 있습니다. 이러면 Html 형인 객체를 2개 만들자 마자 제대로 실행되지 않을 것입니다. 즉 사실상 Html 형은 지금 상태로는 class처럼 사용할 수 없습니다. 그럼 이를 어떻게 해결하면 좋을까요?! 의외로 답은 간단합니다. 여기 지역변수, 인자, this가 아닌 것은 사용하지 않으면 되죠.
html 문서에 출력하는 구조를 변경하기 힘들다!
이 코드에는 매직넘버(그냥 값)가 들어있습니다. 코드 안에 매직넘버가 있으면 변경하기 힘들죠.

document.querySelector('#todo .progress li');

여기서 ‘#todo .progress li’ 이 부분입니다. 이 코드는 지금 만들어놓은 html 문서 코드를 기반해 작성되었죠. 그래서 dom의 위치와 디자인이 다른 곳에서는 사용할 수가 없습니다. 따라서 절대로 하드코딩되면 안되는 겁니다. 이를 해결 하려면 구조는 인자로 받아 들여야 할 것입니다.
이 뿐만 아닙니다. _render에서도 html 문서의 진행 리스트, 완료 리스트에 네이티브의 행위들을 하고 있습니다. 이를 해결하기 위해 두 번째 역할 분리의 대상은 Html 형이 되겠네요. 그렇다면 어떤 역할들로 분리할 수 있을지 알아가보도록 하겠습니다.

우리는 이미 Html 안에 2개의 리스트가 있다는 것을 알고 있습니다. 그리고 여기를 비우고 채우죠. 즉 Html은 리스트를 관리하는 역할을 하고 있습니다. 얘의 진짜 이름은 ListManager가 더 맞겠네요.
그리고 관리를 받을 애들은 List일 거구요. 그럼 List 안에 그려지는 li는 뭐라고 할 수 있을까요? ’얘도 리스트 아닌가…? 이름 정하기 어렵네요ㅠㅠ’ 보통 이런 것을 Item이라고 합니다.
Item은 하나의 Task라는 클래스에 대응하는 돔 구조 하나입니다. List는 Item들을 가지고 있는 것으로 진행 리스트, 완료 리스트 들이 이에 해당하죠. Html은 이름을 바꿔 ListManager가 되고 여러 종류의 List를 가지고 관리하는 역할이죠.

3가지 역할이 나왔습니다. 역할에 맞게 개조 시작!

ListManager

ListManager는 List를 관리합니다. 어떤 List들을 관리할지에 따라 생성되는 구조가 달라질 것입니다. 우리는 진행리스트, 완료리스트만 있다고 가정하겠습니다. 만약 이 외에 보류 리스트를 만들고 싶다면 더 늘어나겠죠.
그렇다면 ListManager에는 생성 시 3가지 인자가 필요합니다.

  • add 함수 : 할 일을 입력하는 form에 할 일 추가 기능을 부여하는 함수
  • pList : 진행리스트를 가리키는 List 형의 인스턴스
  • cList : 완료리스트를 가리키는 List 형의 인스턴스
var ListManager = (function(){
    var ListManager = function(add, pList, cList){
        this._add = add;
        this._pList = pList;
        this._cList = cList;
    };
    var fn = ListManager.prototype = new Renderer();
    fn._init = function(){};
    fn._render = function(){};
    return ListManager;
})();

이제 각 메서드는 어떤 일을 할까요?
1. _init 메서드
add 함수를 실행시켜 form에 할 일 추가 기능을 부여하도록 하고, 진행리스트(pList)와 완료리스트(cList)를 초기화하도록 명령합니다.

fn._init = function(){
    this._add(this._taskManager);
    this._pList.init(this._taskManager);
    this._cList.init(this._taskManager);
};

2. _render 메서드
각 List(pList, cList)에게 화면 비우고 tasks 값에 맞게 할 일을 추가하도록 명령합니다.

fn._render = function(){
    var task;
    this._pList.clear();
    this._cList.clear();
    for(var i = 0; i < this._tasks.length; i++){
        task = this._tasks[i];
        if(task.isComplete()) this._cList.add(task);
        else this._pList.add(task);
    }
};

이 것이 ListManager의 정체입니다. 이제 여긴 네이티브 코드가 하나도 없게 되었습니다. 그리고 add 함수와 각 List에게 일을 맡기죠.

List

List 형은 왜 필요할까요? 이는 pList, cList가 중복되기 때문입니다. 그래서 공통적인 것을 class로 만들어 놓고 쓰려는 것이죠.
List는 생성될 때 자신이 어떤 dom의 리스트인지, 자신 안에 그려질 item을 알아야합니다. 그래서 dom을 찾을 수 있는 selector 정보를 받고, item을 인자로 받아 생성하도록 합니다. 그리고 ListManager를 통해 init, clear, add 메서드가 있다는 것을 알고 있습니다. 그럼 구조를 만들어 보겠습니다.

var List = (function(){
     var List = function(selector, item){
        this._el = selector;
        this._item = item;
    };
    fn = List.prototype;
    fn.init = function(){};
    fn.clear = function(){};
    fn.add = function(){};
    
    return List;
})();

구체적으로 각 메서드가 무슨 일을 하는지 알아보도록 하겠습니다.
1. init 메서드
생성자에서 받아 온 selector를 이용해 자신이 가리키는 dom을 잡아두고, 그 안에 채워질 Item(할 일 항목)을 초기화 합니다.

fn.init = function(taskManager){
    this._el = document.querySelector(this._el);
    this._item.init(taskManager);
};

2. clear 메서드
자신이 가리키는 dom 안을 비워서 채울 준비를 해놓습니다.

fn.clear = function(){ this._el.innerHTML = ''; };

3. add 메서드
자신이 가리키는 dom에 채울 하나의 항목을 Item으로 부터 생성해 받아옵니다.

fn.add = function(task){ this._el.appendChild(this._item.add(task)); };

List 형을 정의했습니다. 이제 각 task에 해당하는 Item을 정의해보겠습니다.

Item

Item은 List안에서 그려질 task 하나 하나 입니다. 그래서 List와 마찬기지로 자기가 가리키는 dom을 찾아낼 selector 값이 필요합니다. 그리고 List에서 호출된 2가지 메서드가 있죠. init, add까지 포함해 Item 구조로 만들겠습니다.

var Item = (function(){
    var Item = function(selector){
        this._el = selector;
    };
    fn = Item.prototype;

    fn.init = function(){};
    fn.add = function(){};

    return Item;
})()

1. init 메서드
생성자에 받아 온 selector를 이용해 본이 될 element를 잡아둡니다.

fn.init = function(taskManager){
    var el;
    this._el = el = document.querySelector(this._el);
    if(el.parentNode) el.parentNode.removeChild(el);
    this._taskManager = taskManager;
};

2. add 메서드
본을 cloneNode 메서드로 복제해 새 element를 만든 뒤 순수 데이터인 task의 내용을 넣어 반환합니다. 이 때 element에는 변경, 삭제 버튼이 있습니다. 이 버튼에 클릭 이벤트를 설정도 합니다.

fn.add = function(task){
    var el = this._el.cloneNode(true);
    el.querySelector('p').innerHTML = task;

    var taskManager = this._taskManager;
    var btns = el.querySelectorAll('input');
    btns[0].onclick = function(){
        taskManager.toggle(task);
    };
    btns[1].onclick = function(){
        taskManager.remove(task);
    };

    return el;
}

안전 제일! shield!top

역할에 맞춰 ListManager, List, Item을 분리 했습니다. 하지만 지금 코드는 부족한 점이 있습니다. 여러 인자들에 대한 방어가 전혀 없습니다. 두번째 강의 때 배웠던 shield 패턴을 추가하도록하겠습니다.

ListManager

ListManager는 생성 시 add 함수와 List 형의 인스턴스 pList, cList를 입력 받습니다. 이 3가지 인자를 검사해 예상치 못한 입력에도 기능 수행을 보장할 수 있도록 하겠습니다.

var ListManager = function(add, pList, cList){
    if(typeof add !== 'function') throw 'add is not a function';
    if(!(pList instanceof List) || !(cList instanceof List)) throw 'list object must be created with new';
    ...
};

List

List는 selector 값으로 특정 dom을 가리킵니다. 그리고 clear하거나 item을 추가하며 그 dom을 관리합니다. 그런데 같은 selector 값을 가진 List가 있다면, 같은 dom을 관리하며 충돌하게 됩니다. dom 소유권 분쟁이 발생!
그래서 똑같은 selector로 List를 생성할 수 없도록 막을 필요가 있습니다. 또한 dom에 추가하려는 item의 형이 진짜 Item인지 확인도 필요하구요.
이를 검사하는 코드가 작성해 좀 더 안전한 List를 만들어 보겠습니다.

var List = (function(){
    var containers = {};
    var List = function(selector, item){
        
        //입력받은 item의 형 검사
        if(!(item instanceof Item)) throw 'item object must be created with new';
        
        //입력받은 selector가 문자열인지 검사
        if(typeof selector !== 'string') throw 'Type of selector must be string';
        
        //같은 selector로 List가 만들어지는 것을 막음
        if(containers[selector]) throw 'Already defined List';
        
        this._el = containers[selector] = selector;
        this._item = item;
    };
    ...
})();

Item

Item에서는 어떤 방어를 해야할까요? Item의 init 메서드 코드를 보면 selector에 해당하는 dom으로 본을 뜨고 없애버립니다. 그런데 똑같은 selector를 가진 Item이 있다면, init 메서드를 실행하는 것에 한발 늦은 Item은 본을 뜨려고 dom을 찾아가더라도 본을 뜰 수가 없습니다.
그래서 똑같은 selector를 가진 Item을 만들 수 없도록 해야합니다.

var Item = (function(){
    var items = {};
    var Item = function(selector){
        
        //입력받은 selector가 문자열인지 검사
        if(typeof selector !== 'string') throw 'Type of selector must be string';
        
        //같은 selector로 Item이 만들어지는 것을 막음
        if(items[selector]) throw 'Already defined Item';
        
        this._el = items[selector] = selector;
    };
    ...
})();

initialize

ListManger, List, Item에는 각각 init 메서드가 있습니다. 이 메서드들을 여러번 호출하게 되면 어떻게 되나요?
Item과 List는 init 메서드에서 문자열로 된 _el로 dom 객체를 찾아 _el에 저장합니다. 그런데 init 메서드를 다시 호출한다면 _el로 dom 객체를 찾을 수 없습니다.
ListManager는 init 메서드에서 add 함수와 List들의 init 메서드를 호출하는 명령을 합니다. add 함수는 입력창을 통해 할 일을 추가할 수 있도록 세팅해주는 함수로 여러번 실행될 필요가 없습니다. 또한 List들의 init함수는 위에서 설명한 것과 같이 여러번 실행되면 안됩니다.
즉, ListManger, List, Item에는 각각 init 메서드는 한번만 실행되어야하는 메서드라고 할 수 있습니다. 하지만 이에 대한 방어가 전혀되어 있지 않아, isInitialized라는 변수로 init 메서드가 실행된 적이 있는지를 체크하도록 하겠습니다.

//ListManager class
var ListManager = (function(){
    var ListManager = function(add, pList, cList){
        ...
        this._isInitialized = false;
    }, fn = ListManager.prototype = new Renderer();
    fn._init = function(){
        if(this._isInitialized) return;
        this._isInitialized = true;
        ....
    };
    ....
})();

//List class
var List = (function(){
    var containers = {};
    var List = function(selector, item){
        ...
        this._isInitialized = false;
    }, fn = List.prototype;
    fn.init = function(taskManager){
        if(this._isInitialized) return;
        this._isInitialized = true;
        ....
    };
    ....
})();

//Item class
var Item = (function(){
    var items = {};
    var Item = function(selector){
        ...
        this._isInitialized = false;
    }, fn = Item.prototype;
    fn.init = function(taskManager){
        if(this._isInitialized) return;
        this._isInitialized = true;
        ....
    };
    ....
})();

완성 To-Do List!top

역할에 맞게 객체들을 분리되었습니다. 이를 가지고 html 문서에 그려질 To-Do List host 코드를 작성해야합니다. 어떤 순서로 코드를 작성할 것인지 부터 생각했습니다.

  1. todo를 TaskManger 인스턴스로 생성한다.
  2. ListManger 인스턴스를 생성해 todo에 Renderer로 세팅한다.
  3. ListManger 인스턴스를 생성하기 위해 add 함수를 정의한다.
  4. ListManger 인스턴스를 생성하기 위해서는 pList, cList를 List인스턴스로 생성한다.
  5. List인스턴스를 생성하기 위해서 각 List에 맞는 Item 인스턴스를 생성한다.

ListManager를 생성하기 위해서는 add함수와 List 인스턴스가, List를 생성하기위해서는 Item 인스턴스가 필요합니다.
따라서 먼저 기초 블럭이 되는 Item부터 시작해 ListManager를 만들어 보도록 하겠습니다.

  1. selector로 Item 인스턴스를 생성한다.
  2. Item 인스턴스로 List 인스턴스를 생성한다.
  3. add 함수를 정의한다.
  4. add 함수와 List 인스턴스로 ListManager 인스턴스를 생성한다.

1. selector로 Item 인스턴스를 생성한다.

var progressItem = new Item('#todo .progress li');
var completeItem = new Item('#todo .complete li');

2. Item 인스턴스로 List 인스턴스를 생성한다.

var progressList = new List('#todo .progress', progressItem);
var completeList = new List('#todo .complete', completeItem);

3. add 함수를 정의한다.

var add = function(taskManager){

    // 할 일의 내용을 입력받는 input 창을 찾는다.
    var title = document.querySelector('#todo .title');

    // 할 일을 추가하는 버튼을 클릭 시 실행될 함수를 정의한다.
    document.querySelector('#todo .add').onsubmit = function(){

        // 입력창의 내용을 꺼내 공백을 없앤다.
        var v = title.value.trim();

        // 입력값이 있다면, todo의 add 메서드를 실행한다.
        if(v) taskManager.add(v);

        // 입력창의 값을 초기화한다.
        title.value = '';

        // onsubmit 함수 실행 후 페이지 재쟁신을 하지 않도록 false를 리턴한다.
        return false;

    };
}

4. add 함수와 List 인스턴스로 ListManager 인스턴스를 생성한다.

var htmlListManager = new ListManager(add, progressList, completeList);

이제 TaskManager 인스턴스를 생성 후 Renderer로 htmlListManager를 세팅하도록 하겠습니다.

var todo = new TaskManager();
todo.setRenderer(htmlListManager);

이로써 To-Do List 프로그램을 완성했습니다! 그럼 전체 코드를 볼까요?! <click! 전체 코드 보러가기>

S70 강의 마무리!top

S70 강의가 끝났습니다. 총 6번의 강의를 듣고 후기를 작성하면서 많은 생각들을 하고 여러 감정들을 느꼈습니다.
제가 생각했던 기초의 수준과는 너무 달랐기에 매주 배우는 개념을 이해하고 정리하는게 벅차기도 했고, 이때까지 해왔던 수박 겉 핥기 식의 공부가 아닌 제대로된 공부를 하고있다는 즐거움도 생겼습니다. 그리고 매 강의를 듣고 작성한 후기 글을 보니 뿌듯하기도 합니다.
강의에서 배운 모든 개념들이 아직 완벽히 숙지되지 않았습니다. 하지만 이는 제가 반드시 암기해야 하는 중요한 내용 들입니다. 그리고 앞으로는 제가 작성하는 모든 코드에 기본적으로 반영되어야 할것입니다.
그러기 위해서는 여러 조각품을 만들어 보면서 역할에 맞게 분리된 조각품을 많이 만들어 봐야합니다. 그러면 언젠간 제가 만드는 조각품도 예뻐지겠죠? ‘ㅎㅎㅎㅎ… 언젠간…
얼마나 예뻐질수 있을까를 기대하며 자바스크립트 언어의 기초! 함수와 객체에 대해 배우는 S70 강의의 마지막 후기글을 마치도록 하겠습니다.
강의를 통해 많은 것을 알려주신 실장님께 감사드리고, 저와 같이 강의를 들으신 분들 모두 수고하셨습니다. 저를 포함해 모두가 앞으로 좋은 개발자가 되길 바랍니다^^

——–S70 스터디 6강(마직막) 영상 공유

s70 자바스크립트 함수와 객체 6강



dimanche | bsidesoft 신입사원
좋은 개발자가 뭔지도 모르고 좋은 개발자가 되고 싶은 초보 개발자입니다.
회사에서는 열심히 교육받고 발전하는 운 좋은 개발자로 일하고 있습니다.