[초보자들] S70 스터디 네번째 후기


자바스크립트 함수와 객체에 대해 알아가보는 S70 네번째 강의가 진행되었습니다.
저번 강의를 통해 To-Do 프로그램 덩어리를 깎아나가 좀 더 좋은 코드 모양으로 변화시켰습니다. 여기서 만족할 수 없습니다. 우리가 만드는 코드 조각이 작품이 될 수 있도록 네번째 후기를 통해 같이 복습해봅시다^^

Refactoringtop

우리가 만들고 있는 조각품은 To-Do 프로그램입니다. 이 프로그램 코드에는 많은 변화가 있었지만, 결과물이 To-Do 프로그램이라는 것에는 변함이 없습니다. 이렇게 결과의 변경없이 코드의 구조를 재조정하는 것을 refactoring 이라고 합니다.
2강부터 시작된 이 코드는 계속해서 내부가 개선되고 있고, 이번 4강을 통해서도 예쁜 코드들로 바꿔나갈 것입니다.

저번 강의시간에 init 함수와 render 함수의 모순점에 대해 언급한 적이 있습니다. <click! 전체 코드 보러가기>

var init, render;
(function(){
    var completeLi, progressLi;

    init = (function(){
        var initHtml = function(){ ... };

        return function(){ ... };
    })();

    render = (function(){
        var renderConsole = function(){ ... };
        var renderHtml = function(){ ... };

        return function(){ ... };
    })();
})();

init 함수에는 initHtml 함수가 있습니다. init 함수에 지금은 없지만 initConsole 함수가 있을 수 있죠. render 함수에는 renderConsole, renderhtml 함수가 묶여져 있습니다.
그리고 init 함수와 render 함수도 익명함수로 묶었습니다. 그 이유는 두 함수만 알고 공유해야하는 값(completeLi, progressLi)이 있기 때문이죠. 즉 두 함수에게만 completeLi, progressLi 값에 대한 권한을 주기위해 익명함수로 만든 스코프에 은닉한 것입니다.
그런데 사실 이 값들은 init 함수와 render 함수 사이에 공유해야될 것이 아닙니다. 더 정확하게는 initHtml 함수와 renderHtml 함수 사이에 공유되야하는 값이었습니다. 이 들 사이에 값을 전달할 수 있는 방법은 이 뿐이었습니다.

completeLi, progressLi 값을 알아야하는 함수에게만 권한을 주기위해 은닉했지만, 실제로는 권한을 가질 필요없는 renderConsole 함수에게도 노출이 된 모순점이 생겼습니다. 이를 그대로 두는 것은 우리가 원하는 바가 아닙니다. 코드 refactoring을 통해 문제를 해결해 나가야합니다.

completeLi, progressLi 값을 공유하는 initHtml 함수와 renderHtml 함수의 관계에 대해 먼저 살펴보도록 하겠습니다.
initHtml 함수는 completeLi, progressLi 값을 renderHtml 함수가 사용할 수 있도록 준비해주고, renderHtml 함수는 준비된 이 값들을 사용해 화면을 출력합니다.
이 때 initHtml 함수가 completeLi, progressLi 값을 어떻게 준비해주냐에 따라 renderhtml 결과가 달라집니다. 아무때나 호출한다고 해서 똑같은 결과를 내지 않죠.

여기서 우리는 새로운 개념을 도출할 수 있습니다. ‘어떠한 행위는 특정 데이터에 기반에서 하는구나’라는 개념이죠.

두 가지 함수top

위의 내용에서 renderHtml 함수를 통해 새로운 개념을 도출했습니다. 어떤 함수는 특정 상태에 기반한 동작을 한다는 것이죠.
이 관점에서 본다면 함수는 두가지로 나눌 수 있습니다.

  • 특정 상태에 바인딩되어 있지 않은 범용적인 함수
  • 특정 상태에 바인딩된 함수

이 두가지 함수가 어떤 특징이 있는지 알아보겠습니다.

범용적인 함수

순수하게 인자, 지역 변수만을 가지고 움직이는 함수를 말합니다. 즉 그 외의 특정 상태에 영향을 받지 않기 때문에 호출할 때 마다 동일한 결과가 나오게 됩니다.
간단하게 이런 함수를 만들어 보자면…

var multiple = function(param){
    var localValue = 10;
    return param * localValue;
}

위의 multiple 함수에서는 인자로 넘겨주는 param 값에 localValue 값을 곱한 결과를 반환하는 함수입니다. localValue가 10이므로 인자로 3을 넘기면 언제나 30이 나오고, 인자로 5를 넘기면 언제나 50이 나옵니다. 즉 어떤 값이 나올지 예측이 가능합니다.

이외에도 범용적인 함수에는 Math.sin 함수, Math.cos 함수 등이 있습니다. 이 함수들도 인자로 넘겨주는 값에만 의존해서 라디안 값을 내보낼 뿐 그 외에 컴퓨터에서 어떠한 상태변화가 있던 영향을 받지 않습니다.

이러한 함수에는 별명이 많습니다. 순수 수학적 함수, 완전 수학적 함수, 수학적 함수, 순수 함수 등의 별명을 가지고 있습니다. 수학에서 함수의 정의는 외부에 있는 변수라는게 존재하지 않습니다. 수식안에 있는 변수 밖에 없죠. 이와 같이 수학에 근접해 있는 함수이기 때문에 순수 수학적 함수라 부르고, 이를 줄여서 순수함수라고 한답니다~

특정 상태에 바인딩된 함수

특정 상태에 변화에 따라서 함수의 작동이 달라지는 함수를 말합니다.
여기서도 간단하게 이런 함수를 만들어 보자면…

var emotion = '행복해';
var actor = function(){
    return emotion;
}
console.log(actor()); // 행복해 가 콘솔에 출력될거에요.

emotion = '피곤해'; 
console.log(actor()); // 피곤해 가 콘솔에 출력될거에요.

..... // 가려진 중간코드
console.log(actor()); // 상태 변화에 따라 나오는 값이 달라서 예측할 수 없습니다.

여기서 actor 함수를 보면 함수 바깥쪽 스코프에 emotion이라는 변수를 참고해서 반환해주고 있습니다. 우리가 호출하는 코드는 똑같습니다. 그러나 호출할 때마다 반환값이 달라집니다.
‘actor();’라는 똑같은 방법으로 호출했는데도 외부 변수, 외부 상태에 영향을 받아서 값이 다르게 나오는 것입니다.
그렇다면 가려진 중간코드 이 후에 호출된 actor 함수의 반환값은 무엇일까요? 중간코드에서 emotion 값에 어떤 변화가 있었는지를 모르기에 무엇이 반환되는지 예측할 수 없습니다.

뭐가 나올까? 🍄💰⭐🌼

예측할 수 없는 actor 함수는 쓸모가 있는건가요? 쓸모가 있으려면 우리는 emotion 값의 변화를 추적할 수 있어야합니다. 하지만 그러긴 어렵죠 어렵죠🙀 안돼안돼~
그래서 우리는 외부 상태를 다~~ 추적하지 않고, 의존하는 상태를 격리시켜 격리된 상태만 의존하는 함수로 만들어 나가는 전략을 세웠습니다. 이렇게 함수에 인접해 있는 것만 의존하게 만들면 훨씬 더 예측하기 쉬워지니까요.

var actor = (function(){
    var emotion = '행복해';
    return function(){
        return emotion;
    }
})();

이 때 actor 함수는 여전히 emotion이라는 바깥 변수의 영향을 받지만, 이제 emotion 값의 변화는 쉽게 일어나지 않습니다. 그래서 아직은 외부상태에 의존하는 함수이지만 안정적으로 emotion 값을 통제할 수 있는 제어를 받기 때문에 이 함수의 동작도 어느정도 예측이 가능해지죠.
그런데 지금 이 코드로는 emotion 값을 영원히 바꿀 수 없습니다.

이제 한 단계 더 나아가 사용자가 emotion 값을 변경할 수 있도록 함수를 추가해봅시다. 그렇다면 사용자에게 노출되어야하는 함수는 총 2가지 입니다. emotion 값을 반환하는 함수와 emotion 값을 변경하는 함수 모두를 노출하기 위해 actor 함수는 object를 반환하기로 합니다.

var actor = (function(){
    var emotion;
    return {
        express: function(){
            return emotion;
        }, 
        setEmotion: function(v){
            emotion = v;
        }
    }
})();

actor.setEmotion('룰루랄라~');
console.log(actor.express()); // 룰루랄라~ 가 콘솔에 출력될거에요.

..... // 가려진 중간코드
console.log(actor.express()); // 룰루랄라~ 가 콘솔에 출력될거에요.

이제 actor 함수는 외부 변수 emotion에 의존함에도 불구하고 setEmotion 함수가 호출되지 않는 이상 ‘actor.express();’는 룰루랄라~가 반환될 것이라는 작동을 보장해 줄 수 있습니다.

객체 만들기top

위의 actor 함수는 상태 변화에 영향을 받지 않게 상태를 가두고 그 안의 함수에게만 상태 변화에 대한 권한을 주어 안정성을 보장합니다. 이를 객체 지향이라고 합니다.
그렇다면 객체는 무엇일까요?
객체는 객체만이 가지고 있는 고유한 상태가 있고, 그 상태를 이용하는 메서드(함수의 별명)으로 이루어져 있습니다. 위의 actor 함수가 객체인거죠.
여기서 메서드는 사전적으로 수단, 도구, 방법이라는 의미를 가지고 있습니다. 객체는 이 메서드를 노출하고, 외부에서는 메서드로 객체를 사용하죠. 즉 메서드는 객체를 사용하는 방법입니다.
객체의 본질은 객체만이 가지 있는 고유한 상태(actor 객체에서는 emotion)이지만, 외부에서 볼 땐 메서드를 사용해 나의 정체성을 파악합니다. 외부에서는 actor 객체를 express, setEmotion 메서드로 객체를 파악하죠. 즉 외부에 표현되는 identity는 메서드입니다.

이 개념을 이용해 To-Do 프로그램에서 객체가 무엇인지 이해하고 만들어 좀 더 안정적인 코드를 만들어야합니다.
To-Do 프로그램에서도 특정 상태에 바인딩되어 있던 함수들이 있었습니다. initHtml, renderHtml이었죠. 여기서 객체화를 시킬 대상은 init일까요? render일까요? html일까요?
지금 함수명은 동사(init, render) + 명사(html)로 이루어져있습니다. 즉 ‘html이 init한다’, ‘html이 render한다’라는 의미죠. 그래서 우리가 객체화 시킬 대상은 html입니다.
html 객체는 특정 상태 progressLi, completeLi 값과, 메서드 init, render가 있는 객체입니다.

var html = (function(){
    var progressLi, completeLi;

    return {
        init: function(){},
        render: function(){}
    }
})();

init 메서드는 기존의 initHtml 함수와 동일하고, render 메서드는 기존의 renderHtml 함수와 동일할 것입니다. 그래서 그 두 개의 함수를 html 객체의 메서드로 옮겨보죠.

var html = (function(){
   var progressLi, completeLi;
 
   return {
       init: function(){
           progressLi = document.querySelector('#todo .progress li');
           completeLi = document.querySelector('#todo .complete li');
           progressLi.parentNode.removeChild(progressLi);
           completeLi.parentNode.removeChild(completeLi);
       },
       render: function(){
           console.log('// 각 목록(진행, 완료)을 비운다.');
           console.log('// 진행 목록(ul)에 progressLi를 채운다.');
           console.log('// 완료 목록(ul)에 completeLi를 채운다.');
           console.log('// 할일 입력창을 비운다.');
       }
   }
})();

객체화 시킬 대상은 html뿐이었을까요? To-Do 프로그램은 mode에 맞게 console이나 html 문서에 출력합니다. 따라서 console 객체도 필요하죠. console은 시스템 객체 이름이니 우리가 만드는 객체이름은 con으로 해요~ <click! console 객체가 뭘까?>

var con = (function(){ 
    return {
        init: function(){
            // 콘솔을 깨끗하게 비워주자!
            console.clear();
        },
        render: function(){
            var task;
            console.log('진행');
            for(var i = 0; i < tasks.length; i++){
                task = tasks[i];
                if(task.state === '진행'){
                    console.log(task.id+'.', task.title+'('+task.state+')');
                }
            }
            console.log('완료');
            for(var i = 0; i < tasks.length; i++){
                task = tasks[i];
                if(task.state === '완료'){
                    console.log(task.id+'.', task.title+'('+task.state+')');
                }
            }
        }//render 메서드 끝
    }// return 문 끝
})();

이로써 html 객체와 con 객체가 만들어졌습니다. 객체들은 각자 알아야 될 권한이 있는 것들만 알게되었습니다.
이제 더이상 console은 progressLi, completeLi 값을 모르게 되었습니다. ‘Refactoring 1에서 언급되었던 모순점도 해결!!’
이렇게 객체의 개념을 알고 권한을 분리한다면 섬세하게 상태를 관리할 수 있게된다는 것을 알게되었습니다.

중복제거top

html, con 객체가 만들어지면서 기존 init, render 함수가 하는 일은 mode에 따라 각 객체의 메서드를 호출하는 기능만이 남았습니다.

var init, render;
(function(){
    init = (function(){
        return function(){
            if(mode === 'console'){
                con.init();
            }else if(mode === 'html'){
                html.init();
            }
        }
    })();
    render = (function(){
        return function(){
            if(mode === 'console'){
                con.render();
            }else if(mode === 'html'){
                html.render();
            }
        }
    })();
})();

이제는 init, render 함수 간에 공유하는 값이 없어 이 둘을 묶는 스코프도 필요 없습니다.
또한 if else로 분기되고 나면 객체의 메서드를 호출하면 되니, 각 함수들만 알고 있는 값이나 함수가 같는 개별적인 스코프도 가질 필요가 없어졌습니다.

var init = function(){
    if(mode === 'console'){
        con.init();
    }else if(mode === 'html'){
        html.init();
    }
}
var render = function(){
    if(mode === 'console'){
        con.render();
    }else if(mode === 'html'){
        html.render();
    }
}

여기서 중복이 보입니다. 각 함수 안에서는 분기하기 위해 mode 값을 확인하는 if문이나 분기 후 호출하는 코드의 형태가 매우 중복적이죠. mode 값에 따라 target(객체)만 바뀔 뿐 init 함수는 init 메서드를, render 함수는 render 메서드를 호출합니다.
To-Do 프로그램에서 바깥으로 노출하는 modeHtml, modeConsole 함수를 이용하면 이 중복을 제거할 수 있습니다.
modeHtml, modeConsole 함수에서 target만을 변경해주고 init, render 함수는 target의 init, render 메서드만을 호출해주는 것이죠.

var todo = (function(){
    ...
    // target의 기본값은 con 객체로 설정했습니다.
    var target = con;
    var init = function(){
        target.init();
    }
    var render = function(){
        target.render();
    }

    var html = (function(){})(...);
    var con = (function(){})(...);
    ...
    return {
        ...,
        modeConsole: function(){
            target = con;
        },
        modeHtml: function(){
            target = html;
        }
    }
})();

이제 init, render 함수에는 중복된 코드가 없습니다.

인터페이스top

우리는 To-Do 프로그램에서 데이터를 예쁘게 출력해 줄 con, html 객체를 만들었습니다.
con 객체와 html 객체는 똑같은 이름의 메서드와 똑같은 인자 구조를 가지고 있습니다. 그렇다는 건 똑같은 정체성을 가지고 있다는 것이죠. 객체는 identity(정체성)가 메서드로 나타난다고 했으니까요.

이렇게 객체간에 정체성이 일치할 때, 그 일치하는 정체성을 클래스라고 합니다.
정체성의 일치는 con 객체와 html 객체에서 ‘init을 가지고 있어’, ‘render를 가지고 있어’ 또는 ‘init과 render 둘 다 가지고 있어~’라고 분해해서 생각할 수도 있습니다.
즉 클래스는 정체성 일치도를 정체성 중에 일치하는 부분이나 부분합 혹은 정체성 전체로 분해해서도 생각할 수 있죠.
그리고 이렇게 일치하는 정체성을 그룹지었다면 이를 인터페이스라고 합니다.

그렇다면 인터페이스는 어떻게 누가 만들까요?
위에서 중복제거한 todo의 init, render 함수에서는 target으로 올 객체가 init, render 메서드를 가지고 있을 것이라고 예상하고 있습니다. target으로 올 객체의 인터페이스는 init, render 메서드를 가지고 있는 것이죠.
그에 맞게 con, html 객체는 인터페이스를 지키고 있죠. 만약 To-Do 프로그램이 새롭게 canvas에 데이터를 출력하고 싶다면, canvas 객체 또한 이 인터페이스를 따라 init, render 메서드를 가지고 있어야하죠.

다시말해 인터페이스는 객체를 사용하려는 측과의 약속입니다.
todo의 init, render 함수가 객체의 init, render 메서드를 호출하기로 했으니까 객체도 init, render 메서드를 만들 책임을 가지기로 약속을 한 것입니다. 이 약속 즉 인터페이스는 사용하는 측의 사정에 의해 만들어집니다. 사용자(todo의 init, render 함수)가 있어야 약속(‘init, render 메서드를 만들게~’)에도 의미가 있으니까요.

Refactoring 2top

객체는 자기가 통제할 수 있는 내부 상태, 인자, 지역변수 만을 가지고 독립적으로 작동해야 합니다.
그런데 con 객체에서 들어가면 안되는 값이 있습니다. 바로 render 메서드의 tasks, STATE_P, STATE_C 입니다. 이 값들은 외부 스코프에 있는 변수입니다. 즉 con 객체가 통제할 수 있는 값이 아닙니다. 이 값들이 들어가는 순간 con은 객체라고 할 수 없죠.. 독립적이지 않은 To-Do 앱의 일부일 뿐..

하지만 con 객체는 tasks, STATE_P, STATE_C가 필요합니다. 이 값을 모르면 콘솔에 그릴 수도 없고, 각 ‘할 일’들의 상태를 판단할 수 없어요😖😖😖
그래서 이 값을 객체가 가질 수 있는 값으로 만들어야합니다. 통제 가능한 내부상태, 인자, 지역변수 중 하나로..

따라서 tasks, STATE_P, STATE_C 값을 외부에서 받아온 인자로 만들겠습니다.

var con = (function(){
    return {
        init: function(){ ... },
        render: function(tasks, STATE_P, STATE_C){
            var task;
            
            console.log('진행');
            for(var i = 0; i < tasks.length; i++){
                task = tasks[i];
                if(task.state === STATE_P){
                    console.log(task.id+'.', task.title+'('+task.state+')');
                }
            }
            console.log('완료');
            for(var i = 0; i < tasks.length; i++){
                task = tasks[i];
                if(task.state === STATE_C){
                    console.log(task.id+'.', task.title+'('+task.state+')');
                }
            }
        }// render 메서드 끝
    }// return 문 끝
});

이제 con객체에 render을 호출할 때 tasks, STATE_P, STATE_C 값을 공급해줘야합니다.

var todo = (function(){
    var tasks, STATE_P, STATE_C;
    tasks = [];
    STATE_P = '진행';
    STATE_C = '완료';
    ...
    var render = function(){
        target.render(tasks, STATE_P, STATE_C);
    }
    ...
})();

이렇게 공급하면 될까요?
con 객체의 render 메서드가 하는 일은 정확하게 말하자면 tasks 변수가 가진 값을 잘 그려내는 것입니다. tasks 자체를 알 필요가 없습니다. 즉 tasks에 관여하고 싶지 않단 말이죠.
따라서 관여하지 않도록 tasks 자체를 넘겨주지 않고 복사본을 넘겨 주도록 합니다. 복사본을 만들기 위해 Object.assign 메서드를 사용하도록 하겠습니다. <click! Object.assign 메서드>

var todo = (function(){
    var tasks;
    ...
    var render = function(){
        target.render(Object.assign(tasks), STATE_P, STATE_C);
    }
    ...
})();

위에서 인터페이스는 사용하는 측의 사정에 의해 만들어진다고 했습니다. 그런데 꼭 그런 것만은 아니었네요.
내부의 사정이나 객체가 수행해야할 사정으로 tasks, STATE_P, STATE_C를 인자로 받는 인터페이스의 변화가 생겼으니까요.
이렇듯 객체와 객체를 사용하는 host 사이에서 끊임없는 상호 작용을 통해 protocol(약속)이 만들어지고 변경되고 파기되고를 반복하고 안정적인 인터페이스가 만들어지게 됩니다.

객체 분리top

html 객체와 con 객체는 완전히 독립적인 객체가 되었습니다. 각자의 지역변수, 인자, 내부의 상태에만 의존합니다.
그렇다면 독립적인 객체가 todo라는 거대한 스코프 안에 속해 있을 필요가 있을까요?

todo는 tasks라는 데이터를 컨트롤 하는 기능을 가지고 있고, 표현은 target 객체의 render 메서드 통해서 합니다.
그렇다는 건 todo는 html, con 객체를 알 필요 없고 데이터를 예쁘게 출력해줄 renderer가 누군지만 공급받으면 됩니다. 그럼 더이상 html, con 객체가 todo 스코프 안에 있을 필요가 없습니다. 분리하도록 하죠!
그리고 modeConsole, modeHtml로 target을 정하는 것이 아니라, 외부로부터 renderer를 받아 설정하는 setRenderer 함수를 만들겠습니다.

var todo = (function(){
    ....
    var target;
    return {
        ...
        setRenderer: function(renderer){
            target = renderer;
        }
    }
})();
var html = (function(){ ... })();
var con = (function(){ ... })();

// todo에게 renderer 알려주기~
todo.setRenderer(html);

객체들 독립 시작!

그런데 외부에서 제대로 된 renderer를 공급해주는지는 어떻게 확신할 수 있죠?
To-Do 프로그램에서 사용하는 renderer는 init 메서드, render 메서드를 가지고 있어야하는데.. 함부로 받을 수 없겠어요!
이 protocol을 준수하고 있는지가 궁금합니다. renderer의 타입을 체크해야겠습니다.
그리고 제대로 된 renderer가 셋팅되는 순간 init 메서드도 호출시켜 render 메서드를 사용할 수 있는 준비를 해줘야되요.

var todo = (function(){
    ....
    var target;
    return {
        ...
        setRenderer: function(renderer){
            if(type of renderer.init !== 'function' || typeof renderer.render !== 'function'){
                return;
            }
            target = renderer;
            target.init();
        }
    }
})();

html 조각하기top

독립한 html 객체에 아직 구현되지 못한 코드들이 있습니다. ‘할 일’을 추가 할 수 있도록 추가 버튼에 이벤트도 줘야하구요. render 메소드도 구현해야합니다.
일단 설계해놓았던 render 메소드 부터 구현해볼까요? 저번 강의에서는 renderHtml이었지만, 지금은 html 객체의 render 메소드입니다.

var html = (function(){
    var progressLi, completeLi;
    return {
        init: function(){
            // progressLi, completeLi 본 저장하기
            progressLi = document.querySelector('#todo .progress li');
            completeLi = document.querySelector('#todo .complete li');
            // 각 목록 비우기
            progressLi.parentNode.removeChild(progressLi);
            completeLi.parentNode.removeChild(completeLi);

            // '할 일' 입력창 찾기
            // 추가버튼에 이벤트 주기(todo.addTask)
        },
        render: function(tasks, STATE_P, STATE_C){
            console.log('// 각 목록(진행, 완료)을 비운다.');
            console.log('// 진행 목록(ul)에 progressLi를 채운다.');
            console.log('// 완료 목록(ul)에 completeLi를 채운다.');
            console.log('// 할일 입력창을 비운다.');
        };
    }
})();

render가 두 상태 progressLi와 completeLi에 의존하는 모습이 제대로 갖춰졌습니다.
그런데 만약 progressLi와 completeLi에 값이 없다면 어떻게 될까요? 즉, init 메서드가 호출되기 전에 render메서드를 호출하면 어떻게 될까요? render에서 progressLi, completeLi의 유효성 검사가 필요합니다!

var html = (function(){
    var progressLi, completeLi;
    return {
        init: function(){ ... },
        render: function(tasks, STATE_P, STATE_C){
            // 값이 없다면 undefined
            if(typeof completeLi === ‘undefined’ || typeof progressLi === ‘undefined’) {
                return;
            }
            ...
        };
    }
})();

이제 render는 progressLi, completeLi가 undefined여도 프로그램에 문제가 생기는 일은 없어졌습니다. 이제 나머지 부분을 설계대로 하나씩 구현해보도록 하겠습니다.

각 목록(진행, 완료)을 비운다.

목록을 비우기 위해 비우려는 목록들을 찾아 각 progress, complete 변수에 할당해 놓겠습니다. 그리고 목록을 비우는 것은 간단하게 그 내용(innerHTML)에 공백 문자를 넣어주죠.

var html = (function(){
    var completeLi, progressLi;
    return {
        init: function(){ ... },
        render: function(tasks, STATE_P, STATE_C){
            if(typeof completeLi === ‘undefined’ || typeof progressLi === ‘undefined’) {
                return;
            }
            //목록 찾기
            var progress = document.querySelector(‘#todo .progress’);
            var complete = document.querySelector(‘#todo .complete’);
            // 각 목록(진행, 완료)을 비우기
            progress.innerHTML = '';
            complete.innerHTML = '';

            console.log('// 진행 목록(ul)에 progressLi를 채운다.');
            console.log('// 완료 목록(ul)에 completeLi를 채운다.');
            console.log('// 할일 입력창을 비운다.');
        };
    }
})();

진행 목록과 완료 목록을 채운다

진행 목록, 완료 목록을 채우기 위해서는 for문을 이용해 tasks의 값을 꺼내야합니다. 그렇다면 각 목록을 채울 때마다 tasks를 for문으로 돌리는 것보단 한번의 for문으로 진행 목록, 완료 목록을 채우도록 합시다.

목록을 채우는 것은 위에서 찾아 놓은 목록(progress, complete)에 progressLi 와 completeLi를 채우면 됩니다.
progressLi 와 completeLi의 본은 유지해 놓아야합니다. 그래서 cloneNode 메서드로 복제한 child라는 변수를 만들도록 합니다. cloneNode(true)를 이용하면 본의 자식 노드까지 모두 복제가 가능합니다. <click! cloneNode 메서드>
그리고 각 목록을 채우는 것은 appendChild 메서드를 사용하겠습니다. <click! appendChild 메서드>

var html = (function(){
    var completeLi, progressLi;
    return {
        init: function(){ ... },
        render: function(tasks, STATE_P, STATE_C){
            if(typeof completeLi === ‘undefined’ || typeof progressLi === ‘undefined’) {
                return;
            }
            //목록 찾기
            var progress = document.querySelector(‘#todo .progress’);
            var complete = document.querySelector(‘#todo .complete’);
            // 각 목록(진행, 완료)을 비우기
            progress.innerHTML = '';
            complete.innerHTML = '';

            //목록을 채운다
            var task, child;
            for(var i = 0; i<tasks.length; i++){
                task = tasks[i];
                if(task.state === STATE_P){
                    child = progressLi.cloneNode(true);
                    progress.appendChild(child);
                }else if(task.state === STATE_C){
                    child = completeLi.cloneNode(true);
                    complete.appendChild(child);
                }
            }
            console.log('// 할일 입력창을 비운다.');
        };
    }
})();

이제 각 목록이 progressLi, completeLi 의 복제본으로 채워질 것입니다. 채워지기만 하면 끝일까요?
아닙니다. progressLi, completeLi 아래에는 ‘할 일’의 title을 넣는 p태그와 ‘할 일’의 상태를 바꾸는 버튼인 input태그와 삭제하는 버튼인 input 태그가 있습니다. p태그에는 내용을 채워주고 두 개의 input 태그에는 기능을 넣어줘야 제대로 채워졌다고 할 수 있겠네요.

p태그의 내용의 경우 child에서 p태그를 찾아 그 내용(innerHTML)을 title값으로 바꿔줍니다.
그런데 두 input에서 사용할 toggle 함수와 remove 함수의 경우 todo에 있습니다. 여기서 그냥 외부 스코프의 todo.toggle 또는 todo.remove로 사용한다면, html은 다시 todo에 의존적인 상태가 됩니다. 안돼😱😱 힘들게 독립적으로 만들었더니…
따라서 tasks와 마찬가지로 인자로 받아야겠습니다. 그럼 언제 todo를 알게해야 할까요? 바로 html 객체의 init시점입니다. setRenderer에서 target이 결정되며 init을 하는데 그때 사용할 todo에 대해 html객체에게 알려주는 것이죠.

var html = (function(){
    var host, completeLi, progressLi;
    return {
        init: function(todo){
            // html 객체를 사용할 host를 설정합니다.
            host = todo;
            ...
        },
        render: function(tasks, STATE_P, STATE_C){
            ...
            //목록을 채운다
            var task, child;
            if(task.state === STATE_P){
                child = progressLi.cloneNode(true); 
                // p 태그에 '할 일' title 채우기
                child.querySelector('p').innerHTML = task.title;
                // 각 input 태그에 기능넣기
                inputs = child.querySelectorAll('input');
                inputs[0].onclick = host.toggle;
                inputs[1].onclick = host.remove;
                
                progress.appendChild(child);
            }else if(task.state === STATE_C){
                child = completeLi.cloneNode(true);
                // p 태그에 '할 일' title 채우기
                // 각 input 태그에 기능넣기

                complete.appendChild(child);
            }
            console.log('// 할일 입력창을 비운다.');
        };
    }
})();

여기서 새로운 문제가 발생합니다. toggle과 remove는 id를 인자를 받습니다. 하지만 위의 형태로는 id값을 전달할 수 없습니다. 그래서 우리는 DOM 안에 id를 기억시키는 방법을 사용합니다. html5에서는 태그에 임의 속성을 넣을 수 있는 ‘data-‘ 속성을 제공합니다. <click! 태그의 data-* 속성>
task.id를 기억시켜야 하기 때문에 우리는 data-task-id 라는 속성을 만들어 id값을 전달하도록 하겠습니다.

var html = (function(){
    var host, completeLi, progressLi;
    return {
        init: function()function(todo){ ... },
        render: function(tasks, STATE_P, STATE_C){
            ...
            //목록을 채운다
            var task, child;
            if(task.state === STATE_P){
                child = progressLi.cloneNode(true); 
                // p 태그에 '할 일' title 채우기
                child.querySelector('p').innerHTML = task.title;
                // 각 input 태그에 기능넣기
                inputs = child.querySelectorAll('input');
                inputs[0].setAttribute('data-task-id', task.id);
                inputs[0].onclick = function(){
                    host.toggle(this.getAttribute('data-task-id'));
                };
                inputs[1].setAttribute('data-task-id', task.id);
                inputs[1].onclick = function(){
                    host.remove(this.getAttribute('data-task-id'));
                };
                
                progress.appendChild(child);
            }else if(task.state === STATE_C){
                child = completeLi.cloneNode(true);
                // p 태그에 '할 일' title 채우기
                // 각 input 태그에 기능넣기

                complete.appendChild(child);
            }
            console.log('// 할일 입력창을 비운다.');
        };
    }
})();

코드 중간에 나온 this는..

onclick에서 this를 쓰면 그 element를 가리키게 된다는 정도로만 알고 넘어가도록 하겠습니다. 자세한건 다음 시간에~
여기까지 진행목록을 채우는 코드만 작성했습니다. 완료 목록은 completeLi를 가지고 비슷한 코드를 작성하면 되겠죠? 그럼 ‘할 일’을 추가 할 수 있도록 추가 버튼에 이벤트를 주는 것만 남았네요~
<click! 완성된 코드 보러가기>

네번째 강의 후기를 마치며..top

저는 강의가 끝난 후 이렇게 후기를 작성하며, 영상도 다시 보기도하고 강의 내용을 적어보며 머릿속을 정리합니다. 처음 듣는 개념, 잘 못 알고 있었던 개념 등이 많아 절대 한번 들어서 이해되지 않으니까요.

그런데 이번 네번째 강의는 더 많이 영상을 돌려보았고, 후기 글이 잘 적어지지 않았습니다.
왜 그럴까요? 아마도 그 앞의 강의에서 배운 개념들도 완벽한 숙지를 못했기 때문 아닐까요?
강의를 듣고 영상을 보고 후기를 작성하고 난 후.. 이제는 알았다고, 후기 작성했으니 끝났다고 생각했었던거 같습니다. 제가 적은 후기를 공개한 후에 다시 보는 일은 잘 하지 않았으니까요. 저는 공부에 대한 오만한 태도가지고 후기 작성의 만족감에 취해 나태해졌나 봅니다.

S70의 강의가 점점 끝나갑니다. 이제 두번밖에 남지 않았어요🙀🙀
하지만 얼마남지 않은 강의에 나올 내용들은 아마도 점점 더 어려워질 것입니다. 그런데 앞의 내용도 제대로 모른 채로 들으면 또 이렇게 힘들겠죠?
첫번째, 두번째, 세번째 그리고 지금 이 네번째 후기를 통해서 모두 다시 복습합시다!
<click! 첫번째 후기 보러가기>
<click! 두번째 후기 보러가기>
<click! 세번째 후기 보러가기>

——–S70 스터디 4강 영상 공유

s70 자바스크립트 함수와 객체 4강 – 1

s70 자바스크립트 함수와 객체 4강 – 2



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