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


자바스크립트 언어의 기초! 함수를 알아가보는 S70 두번째 강의가 진행되었습니다. 첫번째 강의를 통해 함수가 무엇인지, 함수가 왜 필요한지를 살~짝(?) 맛 보았는데요.
어렵긴 하지만 점점 더 깊이 알아갈수록 재밌고 신기하고 아름답기까지한 자바스크립트 함수에 빠져봅시다!

function 특징

첫번째 강의를 통해 우리는 함수가 무엇인지, 함수가 왜 필요한지에 대해 배웠습니다. 그렇다면 이제부터는 이 함수를 잘 만들고, 잘 사용해보면 되겠어요!

예제로 보여드렸던 구구단 프로그램을 기억하시나요?? <구구단 코드 보러가기>
우리는 구구단 프로그램에서 printDan, multiDan, rangeDan, oddDan, evenDan 함수를 만들었습니다. 이 때 함수를 만든 방식은 ‘함수 표현식’이라는 방식이었습니다. 우리의 교재인 ‘인사이드 자바스크립트’에는 함수의 정의에 3가지 방식을 알려줍니다.

  1. 함수 선언문(function statement)
  2. 함수 표현식(function expression)
  3. Function() 생성자 함수

그 중 함수 표현식은 함수를 변수에 할당하는 형태입니다.

우리는 예제에서 이 방식으로 함수를 만들었습니다. 그리고 앞으로도 다른 방식은 사용하지 않고 함수 표현식만 사용해 함수를 정의할 것입니다.

이렇게 정의된 함수를 사용하기 위해 호출하는 방식을 살펴보겠습니다.
함수의 호출은 식별자(함수의 이름)를 적은 후 괄호를 열고 지정된 인수를 채워넣은 후 괄호를 닫으면 됩니다. 이렇게요~

//sum 함수를 호출
sum(3, 4);

우리는 함수를 정의하고 호출하는 법을 알았고, 사용하기 전에 함수의 몇 가지 중요한 특징을 알아보겠습니다!

< 잠깐만~ 우리 이제 한번 해봐요~ >

잠깐만! ‘아~ 그렇구나’하고 지나갈 뻔 했지만, 엄청난 매의 눈을 가지신 실장님이 짚어주신 괄호에 잠시 집중을 해봅니다. 괄호는 코드의 여기 저기 자주 보입니다. 위와 같이 함수를 호출할 때 호출 연산자로 사용되는 것은 알고 있습니다. 또 어디에 사용될까요?
3 + ( 1 + 2 ) 과 같은 계산을 할 때 자연스럽게 괄호 안의 연산부터 하시죠? 이렇게 연산자가 실행될 순서에 영향을 주는 우선순위 연산자로도 사용되고, for/switch/if 등 여러 문의 일부로 사용되기도 합니다.

자~ 이제 진짜 자바스크립트 함수의 특징을 알아볼까요~

함수는 정의하고 나면 그 이후로 부터 호출할 수 있다!
// ① sum 함수 정의 전 호출
console.log(sum(3,5));

// sum 함수 정의
var sum = function(a, b){
      return a + b;
}

아직 정의하지 않은 함수를 호출(①)하면 자바스크립트는 TypeError: sum is not a function 이란 에러가 발생하고 즉시 멈추게 됩니다.

자바스크립트 함수는 무조건 반환값이 있다!

함수의 3요소! 입력, 출력, 이름! 그 중에 출력에 해당하는 것이 반환값입니다. 함수가 실행되고 종료하면 그 함수로부터 값이 반환되는 거죠. 함수 내부에 return 문을 보셨을 것입니다. return 키워드 뒤에 오는 값이 반환되는 것이죠. 하지만 return 문이 없어도 에러가 발생하지 않던데…?
그렇습니다. 자바스크립트의 함수 안에 return 문을 작성하지 않아도 컴파일러가 return undefined; 를 붙여준다고 합니다.

식별자에 할당할 수 있다!

함수 표현식에서 보셨다시피 함수는 식별자에 할당할 수 있습니다. 그 이유는 자바스크립트 함수는 값이기 때문이죠. 그래서 할당이 가능합니다.

여기까지 함수를 정의하는 법, 호출하는법, 그리고 몇 가지의 특징들에 관한 내용이었습니다.

이 내용들은 교재에도 설명이 되어 있습니다. 하지만 그냥 책의 설명을 보았을 때보다 훨씬 흥미롭지 않나요? 그 이유는 아마도 이 내용이 왜 있는지를 알고 넘어가기 때문인 것 같습니다. ‘왜?’라는 질문은 참 중요한 것 같습니다. 혼자서 책을 볼 때 그런 생각이 잘 안드는 건 함정ㅠㅠ

즉시 실행 함수(IIFE; Immediately Invoked Function Expressions)

함수는 중복제거용으로만 사용되는 것이 아닌 Scope(유효범위), Encapsulation(캡슐화), Life Cycle(생명주기)을 위해 사용한다는 것을 배웠습니다.
즉 scope를 만들어 접근권한을 통제하고 변수의 라이프사이클을 조절해 충돌을 막는 용도로도 사용된다는 것입니다. 이러한 용도로 함수를 만들 때 사용하는 패턴이 바로 즉시 실행 함수입니다.

즉시 실행 함수의 기본 형태는 아래와 같습니다. 함수 리터럴을 괄호로 둘러싼 다음 함수가 바로 호출될 수 있게 괄호쌍을 추가하는 형태죠.

(function () {
    // statements
})()

이 패턴을 함수 표현식과 비교하자면.. 함수 표현식은 함수를 정의하고, 변수에 함수를 저장한 후 호출해 실행하는 과정을 거치지만, 즉시 실행 함수는 함수를 정의하고 바로 실행해 이런 과정을 거치지 않습니다. 그렇다보니 위와 같은 형태로 쓴다면 식별자에게 할당하지 않아 실행 후 다시 호출할 수가 없다는 특징이 있습니다.

아직 초보 중에 초보인 저에겐 어색한 함수 패턴이었습니다. 하지만 함수로 scope가 만들어지는 자바스크립트에서 변수 이름 충돌 문제를 방지하기 위해 IIFE 패턴이 많이 사용 된다고 합니다. 그래서 이 형태가 무엇인지, 무엇을 위해 사용되는지는 꼭 알아둬야할 것입니다.

Lexical Environment

계속 계속 중요하게 언급되는 scope가 무엇인지 좀 더 알아보겠습니다.

색칠된 scope 영역에서는 외부의 g, f와 내부의 k, h, v1, v2 변수를 모두 인식하고 있습니다. 하지만 외부 scope에서는 색칠된 scope 내부의 k, h, v1, v2 값을 모르죠.

이 때 인식가능한 변수를 Lexical이라하고, Lexical의 인식범위는 scope에 따라 조정이 됩니다. 안쪽 scope일수록 Lexical의 인식범위가 커져 바깥쪽보다 더 많은 변수를 사용가능하도록 말이죠.

즉 Lexical을 정의하는 범위는 scope에 따라 결정되므로 이 scope를 Lexical Environment라고 합니다.

must be

우리는 이번 강의를 통해서 함수를 배우고 문제를 풀어나가는 센스 키우기를 연습할 것이라고 했었습니다. 그래서 예제로 To-Do List를 구현해 보며 개발의 흐름이 어떻게 흘러가는지 알아갈 것입니다.

요즘 To Do List 프로그램을 보면 많은 기능들이 있지만.. 우리 예제에서는 위의 이미지 정도의 기능만이 있는 프로그램을 만들어 볼 것입니다.

To Do List 프로그램을 표현하면 위의 이미지같은 화면을 떠올립니다. 이는 우리가 만들 것이 무엇인지 알기 위해 위의 이미지로 표현했을 뿐입니다. 그렇다면 실제 데이터는 어떻게 생겼을까요? 각 할일의 title과 진행중인건지 완료된 것인지를 나타내는 state가 있지 않을까요? 이를 보기 쉬운 표와 Javascript 객체로 나타내보겠습니다.

title state
할일1 완료
할일2 완료
할일3 진행
//Javascript 객체로 나타낸 To Do List 실제 데이터
[ 
  { title: '할일1',  state: '완료' },
  { title: '할일2',  state: '완료' },
  { title: '할일3',  state: '진행' }
] 

사실 뷰는 데이터만 있다면 어떤 모양이든 만들 수 있을 것입니다. 어플로 만들어 볼 수도 있고, 브라우저 화면으로 볼 수도 있고, 콘솔로 볼 수도 있습니다. 즉 프로그램의 핵심은 데이터이고 화면은 예쁜 데이터 뷰입니다.

그럼 프로그램의 핵심! 데이터가 저렇게 생겼다는걸 어떻게 알 수 있을까요? 데이터란 무엇을 말하는 걸까요?
데이터란 ‘기억해야만 하는 것’을 말합니다.
예를 들어 현실세계에 dimanche라는 사람은 다양한 값을 가지고 있습니다. 이름, 나이, 키, 몸무게, 외모, 성격, 가족관계, 취미, 특기, 좋아하는 음식, 직업, 학번, 전공, 학점 등등…
dimanche라는 사람을 학생으로써 기억해야하는 것들은 이름, 학번, 전공, 학점일 것이고, 소개팅 상대로써 기억해야하는 것들은 이름, 나이, 키, 몸무게, 외모, 성격, 직업일 것입니다.
이렇게 어떤 목적을 가지고 보느냐에 따라 기억해야만 하는 데이터가 달라집니다. 이런 데이터(필요한 값)를 모델이라고 하고, 현실세계의 값 중에서 모델을 추출하는 것을 모델링이라고 합니다. 모델링에 성공해야 데이터의 실체를 알 수 있고, 데이터의 실체를 알아야 어떻게 편집할 수 있을지를 알 수 있게 됩니다.

위에서 To Do List에서의 모델링을 했으니 이번엔 데이터를 편집하기 위한 행위(behavior)를 찾아보겠습니다.
할일을 추가할 수 있고, 할일을 삭제할 수 있고, 할일의 상태를 진행 또는 완료로 변경할 수도 있고, 할일을 편집할 수도 있겠네요.
데이터를 편집하기 위한 행위들이 다 찾은 것 같나요? 저는 방금 필통에 권총을 달았습니다.

< 빵야? >

권총이 달린 필통에서 연필을 찾던 저는 방아쇠를 당겼네요.
이는 필통이 잘 못 만들어진 것도 아니고, 권총이 잘 못 만들어진 것도 아닙니다. 하지만 필통을 사용하는데 필요하지 않은 권총을 달아놔서 생긴 사건이죠.

우리가 만드려했던 To Do List 프로그램과 behavior 목록을 비교해보면 할일을 편집하는 기능은 사용되지 않습니다. To Do List 이미지에 수정버튼은 없잖아요? 즉 필통에 달린 권총과 같이 필요하지 않는 것을 만들어 놓은 것이죠. 이는 누군가에게 언제든지 방아쇠가 당겨져 사건(버그)을 발생시킬 것입니다.

이제 진짜 To Do List의 behavior는 무엇이 있는지 그리고 각 behavior의 로직을 정리해봅시다.

  1. 할일의 삭제한다 : removeTask(id)
  2. 할일을 추가한다 : addTask(title)
  3. 할일의 상태를 변경한다 : changeState(id, state)

(1) removeTask(id)
일단 할일을 삭제하려면 여러개의 데이터 중 어느 것인지 판단을 해야합니다. 하지만 title과 state는 중복이 될 수 있습니다. 그래서 화면에 보이진 않지만 각 레코드의 유일값으로 id를 추가합니다. 그럼 실제 데이터는 아래의 표와 같아집니다.

id title state
0 할일1 완료
1 할일2 완료
2 할일3 진행

이제 removeTask 함수를 호출 시 id 값을 받아 레코드 중 해당 id를 가진 데이터를 찾아 삭제합니다. 그리고 화면을 갱신합니다.

(2) addTask(title)
title 값은 받고, id는 데이터에서 중복되지 않는 값을 부여하고, state는 ‘진행’인 값으로 레코드를 만듭니다. 이를 데이터에 저장합니다.
그리고 화면을 갱신합니다.

(3) changeState(id, state)
id와 변경할 state 값을 받습니다. 레코드 중 해당 id를 가진 데이터의 state를 변경합니다.
그리고 화면을 갱신합니다.

각 로직마다 갱신되는 화면의 모습은 다릅니다.
addTask는 화면에 새로운 할일이 보여질 것이고, removeTask는 화면에 선택한 할일이 사라질 것이고, changeState는 할일이 진행과 완료 목록 사이를 옮겨다닐 것입니다.

하지만 정리된 로직을 살펴보면 ‘화면을 갱신한다’로 표현하고 있습니다.
위에서 언급한 ‘화면은 예쁜 데이터 뷰이다’를 기억하시나요?
화면을 변화시키는 것은 각 로직마다 일어나는 것이 아닌 데이터를 기반으로 전체를 지우고 그려내고 지우고 그려내고를 반복하면 됩니다. 따라서 데이터를 가지고 화면을 그리는 render 함수를 만들어 위임하도록 합시다. render 함수에서는 데이터에 따라 원하는 모양으로 뷰를 구현하면됩니다.

여기까지 우리가 기획한 To Do List를 보고 모델을 찾아 정리했고 모델을 통해 behavior를 도출했습니다. 이제는 behavior를 코드로 작성해 프로그램을 완성하는 일만 남았네요.

짜잔! To Do List

위에서 정의한대로 일단 함수를 작성해봅시다!

var tasks = [];
var id = 0;

var addTask = function(title){
    tasks.push({id:id++, title:title, state:'진행'});
    render();
};

var removeTask = function(id){
    for(var i =0; i< tasks.length; i++){
        if(tasks[i].id === id){
            tasks.splice(i,1);
            break;
        }
    }

    render();
};

var changeState = function(id, state){
    for(var i =0; i< tasks.length; i++){
        if(tasks[i].id === id){
            tasks[i].state = state;
            break;
        }
    }
    render();
};

var render = function(){
    console.log('진행');

    var task;

    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+')');
        }
    }
};

이제 작성된 코드가 접근권한을 철저하게 분리되어 있는지, 불필요한 코드가 남발되진 않았는지, 한줄 한줄을 왜 이렇게 썼는지 설명할 수 있게끔 작성되었는지를 확인하겠습니다.
코드 전체를 한꺼번에 짚어나가는 것은 보기 힘들 수 있습니다. 그러므로 조금씩 나눠 살펴 보도록 하겠습니다.

먼저 전역scope에 선언된 tasks, id 변수와 addTask 함수입니다.

var tasks = [];
var id = 0;
var addTask = function(title){
    tasks.push({id: id++, title: title, state: '진행'});
    render();
};

이 코드에서 짚어볼 내용들입니다.
(1) id 변수의 격리
변수 tasks는 전체 데이터를 저장하는 변수 입니다. 이는 프로그램 전체에서 사용할 수 있어야하기에 전역 scope 범위에 있는 것이 좋습니다. 하지만 새로 만들질 레코드에 유일값을 주기 위해 사용될 변수 id는 addTask 함수에서만 사용되는 값입니다.
그래서 addTask만 id를 인식 할 수 있도록 IIFE(즉시 실행 함수) 패턴을 사용해 scope를 만들 것입니다.

var tasks = [];
var addTask = (function(){
     var id = 0;
     return function(title){
          tasks.push({id: id++, title: title, state: '진행'});
          render();
     }
})();

(2) 매직넘버의 상수화
매직넘버란 소스 코드 안에 작성된 구체적인 값을 의미합니다. tasks에 새로운 할 일을 push 할때 state에 할당되는 ‘진행’이라는 값처럼 말이죠.
지금 만들어 나가는 프로그램에서 state에는 ‘진행’, ‘완료’라는 문자로 표현하지만 이는 언제든지 변할 수 있습니다. 그래서 이런 값을 대체할 어휘를 만들어 주도록 합시다.

var tasks = [];
var STATE_P = '진행';
var STATE_C = '완료';

var addTask = (function(){
     var id = 0;
     return function(title){
          tasks.push({id:id++, title:title, state:STATE_P});
          render();
     }
})();

다음으로 removeTask 함수를 살펴보겠습니다.

var removeTask = function(id){
    for(var i =0; i< tasks.length; i++){
        if(tasks[i].id === id){
            tasks.splice(i,1);
            break;
        }
    }
    render();
};

이 코드에서 짚어볼 내용들입니다.
(1) 입력값(id)의 validation과 shield pattern
validation이란 유효성을 확인하는 것을 말합니다.
removeTask 함수는 tasks에서 어떤 레코드를 삭제할지를 판별하기 위해 id라는 값을 인자로 받습니다. id와 같이 외부에서 입력받는 인자는 우리가 예상한 것과는 다른 값들이 들어올 수 있습니다. 예를 들어 숫자가 아닌 문자로 ‘메롱~’을 입력 받을 수도 있고, tasks에 없는 id값일 수도 있습니다. 이는 분명 올바른 id값은 아닙니다. 그렇기 때문에 validation을 통해 안전한 값인지를 확인해야합니다.
지금 코드에서는 사실 유효하지 않은 id라고 해도 아무런 반응을 하지 않습니다. 하지만 이렇게 유효하지 않은 값으로 로직을 실행시키는 것은 버그를 발생시키고 예고없이 프로그램이 죽어버리는 상황이 생길 수도 있습니다.
그래서 유효하지 않은 값이라면 로직으로 들어가지 못하게 shield를 만들어 로직을 보호해야합니다. 이러한 코드의 형태를 shield pattern이라고 합니다.

var removeTask = function(id){
      var isValid = false;
      for(var i = 0; i < tasks.length; i++){
            if(tasks[i].id === id){
                isValid = true;
                break;
           }
      }
      if(isValid === false) {
           console.log('removeTask : invalid id - ' + id);
           return;
      }

      //입력 값들의 유효성 검사를 통화 후 로직이 진행되도록 한다.
      for(var i =0; i< tasks.length; i++){
          if(tasks[i].id === id){
               tasks.splice(i, 1);
               break;
           }
      }
      render();
}

(2) White List
로직에서는 validation 후 부산물로 받아온 id를 사용하고 있습니다. 로직으로 들어오기 전 validation을 했지만, 그래도 잘못된 값을 피할 수 있는 완전히 바른 값이 필요합니다.
그래서 validation 되기 전의 값을 validation된 값(검증된 값)을 사용해 로직을 전개시키도록 하겠습니다. 이때 검증된 값을 White List라고 합니다.

var removeTask = function(id){
      // 검증된 id가 절대 될 수 없는 값인 false로 초기화한다.
      var whiteList = { id: false };
      for(var i = 0; i < tasks.length; i++){
            if(tasks[i].id === id){
                whiteList.id = id;
                break;
           }
      }
      // id가 검증된 값인지 확인한다.
      if(whiteList.id === false) {
           console.log('removeTask : invalid id - ' + id);
           return;
      }
      //입력 값들의 유효성 검사를 통화 후 로직이 진행되도록 한다.
      for(var i =0; i< tasks.length; i++){
          if(tasks[i].id ===  whiteList.id){
               tasks.splice(i, 1);
               break;
           }
      }
      render();
}

(3) Native Binding 제거
함수는 데이터 편집하는 행위로 이루어집니다. 이 때 순수한 데이터를 편집하는 행위만으로 이루어져 있는 함수를 순수 함수라고 합니다. 예를 들어 예쁜 데이터 뷰를 그리는 것을 render 함수에게 시키고 순수한 데이터를 추가하는 행위만 있는 addTask 함수를 말합니다. 순수함수에서 처럼 순수하게 데이터만을 다루는 로직을 non-native 로직이라고 합니다.

그렇다면 반대로 native한 로직은 무엇일까? 순수한 데이터 편집을 방해하고 특수한 시스템에 binding 되는 로직을 말합니다. removeTask 함수 코드에서 사용자에 경고를 보내기 위해 console에 경고 문구를 출력해 console이 있는 플랫폼에 binding된 로직이 그 예입니다.
이런 native 로직에는 단점이 있습니다. binding된 특수한 시스템 이외에는 사용할 수 없다는 것입니다. console이 있는 플랫폼 전용이 된 removeTask 함수는 console이 없는 플랫폼에서는 사용할수 없는 함수가 된 것이죠. 즉 native 함수는 플랫폼에 의존하는 함수라는 단점을 가집니다.

그럼 어떻게하면 removeTask 함수가 non-native한 함수가 될 수 있을까요?
native 로직을 감추는 방법은 native 로직을 처리할 다른 함수에게 위임하는 수 밖에 없습니다. 그래서 warning 함수를 만들어 ‘사용자에게 경고를 준다’는 행위는 warning 함수에게 넘겨주고 warning 함수가 알아서 native 로직을 처리하도록 위임하는 것이죠. 이를 Delegation이라고 합니다.

var removeTask = function(id){
      // 검증된 id가 절대 될 수 없는 값인 false로 초기화한다.
      var whiteList = { id: false };
      for(var i = 0; i < tasks.length; i++){
            if(tasks[i].id === id){
                whiteList.id = id;
                break;
           }
      }
      // id가 검증된 값인지 확인한다.
      if(whiteList.id === false) {
           warning('removeTask : invalid id - ' + id);
           return;
      }
      //입력 값들의 유효성 검사를 통화 후 로직이 진행되도록 한다.
      for(var i =0; i< tasks.length; i++){
          if(tasks[i].id ===  whiteList.id){
               tasks.splice(i, 1);
               break;
           }
      }
      render();
}

마지막으로 changeState 함수를 보죠~

var changeState = function(id, state){
    for(var i =0; i< tasks.length; i++){
        if(tasks[i].id === id){
            tasks[i].state = state;
            break;
        }
    }
    render();
};

이 코드에서는 위의 removeTask 함수에서 알아본 White List와 shield pattern을 동시에 적용하고 Native Binding 제거하는 연습을 해볼 수 있겠네요.

var changeState = function(id, state){
     var whiteList = { id: false, state: ’’ }
     for(var i = 0; i < tasks.length; i++){
          if(tasks[i].id === id){
               whiteList.id = id;
               break;
          }
     }
     if(whiteList.id === false) {
          //console.log가 아닌 warning 함수에게 경고하는 행위를 위임
          warning('changeState : invalid id - ' + id);
          return;
     }
     if(STATE_P !== state && STATE_C !== state){ 
          //console.log가 아닌 warning 함수에게 경고하는 행위를 위임
          warning('changeState : invalid state - ' + state);
          return;
     }else{
          whiteList.state = state;
     }
     //입력 값들의 유효성 검사를 통화 후 로직이 진행되도록 한다.
     for(var i =0; i< tasks.length; i++){
          if(tasks[i].id === whiteList.id){
               tasks[i].state = whiteList.state;
               break;
          }
     }
     render();
};

드디어 3가지 behavior가 잘 작성되었는지 확인했습니다. < 강의에서 완성된 코드 보러가기 >

2강을 마치며

저는 이번 강의의 예제를 가지고 예쁜(?) 데이터 뷰를 만들어보았습니다.
함수의 괄호 안에 인자를 넣고 실행버튼을 눌러보세요~

제가 입사 후 슬롯머신, 타임머신 에디터, To-Do List, 테트리스를 만들어보는 실습을 했습니다. 그 중에 To-Do List 실습을 해보면서 ‘쉽고 간단한 프로그램이라고 생각한 To-Do List가 이렇게 어려울 줄이야ㅠㅠ’라는 생각을 했던게 아직 잊혀지지 않습니다.
이번 강의 예제를 통해 한번 더 해보는 것이지만, 역시 아직도 저에겐 어렵고 배울 개념들이 많다는 것을 다시 한번 느끼게 되었습니다.

이번에 배운 내용들을 되새겨가며 열심히 정리했습니다. 하지만 강의 때 나왔던 개념들 중에 빠진 것들도 많을 것입니다. 어느 것 하나 중요하지 않거나 덜 중요한 내용이 없었기에 영상도 다시보고 필기도 다시보며 이번 강의의 모든 내용을 숙지해야 할 것입니다.

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

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



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

%d 블로거가 이것을 좋아합니다: