프론트엔드/Javascript

자바스크립트 문법 13 - 함수(function)의 활용

syleemomo 2022. 1. 26. 14:03
728x90

 

함수 참고문서 2

 

함수 - JavaScript | MDN

함수는 JavaScript에서 기본적인 구성 블록 중의 하나입니다. 함수는 작업을 수행하거나 값을 계산하는 문장 집합 같은 자바스크립트 절차입니다. 함수를 사용하려면 함수를 호출하고자 하는 범

developer.mozilla.org

 

* 함수 스코프 - 렉시컬 스코프(Lexical Scope) 또는 정적 스코프(Static Scope)

 

스코프 참고 블로그

 

자바스크립트 - 렉시컬 스코프(Lexical Scope)

들어가기에 앞서, Closure(클로져)를 이해하기 위해서는 반드시 렉시컬 스코프(Lexical Scope)를 이해해야 한다. 렉시컬 스코프란(Lexical Scope)란? 함수를 어디서 호출하는지가 아니라 어디에 선언하였

ljtaek2.tistory.com

 

const fruit = 'apple' // 전역변수

// 글로벌 스코프에 선언된 printA 함수
function printA(){
    const fruit = 'banana' // 지역변수
    console.log(fruit)
}

printA() // banana

첫번째 fruit (apple) 은 전역변수이다. 두번째 fruit (banana) 은 지역변수이다. printA 함수는 전역함수이다. 이유는 전역변수와 동일한 위치인 글로벌 스코프에 선언되어 있기 때문이다. 이 경우 글로벌 스코프에 선언된 함수는 기본적으로 전역변수를 참조한다. 즉, apple 을 참조한다. 하지만 printA 함수 내부에 전역변수 이름과 동일한 지역변수 fruit 이 존재하기 때문에 우선순위에 의하여 fruit 은 banana 가 된다. 

 

const fruit = 'apple'

function printA(){
    const fruit = 'banana'
    printB()
}

function printB(){
    console.log(fruit) 
}

printA() // apple
printB() // apple

자바스크립에서의 함수 스코프는 렉시컬 스코프 또는 정적 스코프이다. 이 말의 의미는 함수를 어디서 호출하였는지가 아니라 함수를 어디서 선언하였는지에 따라 상위 스코프가 결정된다는 뜻이다.

위 코드를 보면 전역변수 fruit 과 지역변수 fruit 이 존재한다. printA 함수를 실행하면 지역변수 fruit 에 의하여 banana 가 출력될것 같지만 실제로는 아니다. 이유는 fruit 을 출력하는 코드가 printB 함수에 존재하는데 printB 함수는 글로벌 스코프에 선언되었으므로 printB 함수 내의 fruit은 전역변수 apple 을 가리킨다. 

정리하면 printB 함수 내의 fruit 변수는 printB 함수가 호출될때 결정되는 것이 아니라 선언될때 결정되는 것이다. 

 

const global = 3 // 전역변수 (상위 스코프)

function globalFunction(){
    const local = 1 // 지역변수
    return global + local
}

console.log(globalFunction())

함수스코프는 렉시컬 스코프이므로 자신의 지역변수와 선언된 위치에서 상위 스코프의 변수에 접근 가능하다. 위 코드에서의 상위 스코프는 전역 스코프이므로 전역변수를 사용할 수 있다. 

const global = 3 // 전역변수 (상위 스코프)

function globalFunction(){
    const localOuter = 2 // 외부함수 스코프
    function localFunction(){
        const local = 1 // 지역변수 
        return global + localOuter + local
    }
    return localFunction()
}

console.log(globalFunction())

함수스코프는 렉시컬 스코프이므로 자신의 지역변수와 선언된 위치에서 상위 스코프의 변수에 접근 가능하다. 위 코드에서의 지역변수는 local 이다. 상위스코프는 외부함수 스코프와 전역 스코프이다. 그러므로 상위 스코프의 변수는 localOuter 와 global 이다. localFunction 이 자신보다 상위 스코프의 변수를 계속 검색해나가는 것을 스코프 체인이라고 한다. 

 

 

* 콜백함수 (callback function)

콜백함수는 함수의 인자로 전달되어 함수의 내부에서 실행되는 함수이다. 

const numbers = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]

function map(f, arr){
    const newArr = []
    for(let i in arr){
        newArr[i] = f(arr[i])
    }
    return newArr
}

function str2num(element){
    return parseInt(element)
}

const numbersParsed = map(str2num, numbers)
console.log(numbersParsed)

위 코드는 자바스크립트의 배열 메서드 map 을 함수를 이용하여 동일하게 구현한 것이다. map 함수의 파라미터는 두개이다. 첫번째 파라미터는 콜백함수 f 이며, 두번째 파라미터는 배열이다. map 함수는 각각의 배열요소에 대하여 콜백함수(str2num 함수)를 실행시킨 다음 결과값을 새로운 배열(newArr)에 저장한다.   

function add(a, b){
    return a + b
}
function subtract(a, b){
    return a - b
}
function multiply(a, b){
    return a * b
}
function divider(a, b){
    return a / b
}

function calculator(mode, a, b, ...funcs){
    let ret = null
    switch(mode){
        case '+':
            ret = funcs[0](a, b)
            break
        case '-':
            ret = funcs[1](a, b)
            break
        case '*':
            ret = funcs[2](a, b)
            break
        case '/':
            ret= funcs[3](a, b)
            break
    }
    return ret
}

const ret1 = calculator('+', 3, 4, add, subtract, multiply, divider)
const ret2 = calculator('-', 3, 4, add, subtract, multiply, divider)
const ret3 = calculator('*', 3, 4, add, subtract, multiply, divider)
const ret4 = calculator('/', 3, 4, add, subtract, multiply, divider)

console.log(ret1)
console.log(ret2)
console.log(ret3)
console.log(ret4)

위 코드는 계산기 예제이다. calculator 함수는 첫번째 인자로 설정된 mode 에 따라 콜백함수인 add, subtract, multiply, divider 중 하나를 선택하여 실행한다. 그리고 계산 결과값을 반환한다. funcs 는 이전 수업에서 배운 나머지 매개변수(...)을 이용하여 콜백함수들을 배열로 묶은 것이다. 결과는 아래와 같다. 

계산기를 콜백함수로 구현한 결과

 

클로저 참고문서

 

클로저 - JavaScript | MDN

클로저는 함수와 함수가 선언된 어휘적 환경의 조합이다. 클로저를 이해하려면 자바스크립트가 어떻게 변수의 유효범위를 지정하는지(Lexical scoping)를 먼저 이해해야 한다.

developer.mozilla.org

 

* 클로저(Closure) 개념

function initCount(){
    let cnt = 0
    function increaseCount(){
        cnt++
        return cnt
    }
    return increaseCount
}

const cnt1 = initCount() // 클로저 (독립적인 실행환경) 생성
const cnt2 = initCount() // 클로저 (독립적인 실행환경) 생성
const cnt3 = initCount() // 클로저 (독립적인 실행환경) 생성

cnt1() 
console.log(cnt1()) 

cnt2()
cnt2()
console.log(cnt2())

cnt3()
cnt3()
cnt3()
console.log(cnt3())

클로저는 한마디로 말하면 독립적인 실행환경이다. 외부함수 initCount 를 실행하면 내부함수 increaseCount 가 반환되면서 지역변수 cnt 값을 저장하는 클로저(독립된 실행환경)를 형성한다. 

위 코드에서 클로저가 형성되면 외부함수 initCount 의 실행이 종료되어도 지역변수 cnt 는 메모리에서 사라지지 않는다. 또한, 반환된 내부함수 increaseCount (cnt1, cnt2, cnt3)는 외부함수 initCount 의 지역변수 cnt 에 접근 가능하다. 내부함수 increaseCount 가 선언된 위치(지역함수)는 initCount 내부이므로 렉시컬 스코프에 의하여 지역변수 cnt 값을 사용할 수 있게 된다. 

즉, 내부함수가 자신에게 존재하지 않는 외부함수의 지역변수까지 메모리에 저장하고 독립적인 실행환경을 만들어 가두는 것을 클로저(폐쇄회로처럼 가둔다)라고 한다. 

 

클로저는 아래와 같은 몇가지 특장점을 가지고 있다. 

const cnt1 = initCount() // 클로저 (독립적인 실행환경) 생성
const cnt2 = initCount() // 클로저 (독립적인 실행환경) 생성
const cnt3 = initCount() // 클로저 (독립적인 실행환경) 생성

동일한 실행환경을 손쉽게 여러번 생성할 수 있다. 위 코드에서 외부함수 initCount 를 실행할때마다 지역변수 cnt 값을 저장하는 새로운 실행환경이 동일하게 만들어진다. 각 실행환경은 서로 다른 메모리 주소에 생성되므로 각 실행환경에 저장된 cnt 값은 다른 메모리 주소에 기억되어 다른 값을 가질수 있다. 

cnt1() // 지역변수 cnt 는 함수실행에 의해서만 변경가능함
console.log(cnt1())

cnt2() // 지역변수 cnt 는 함수실행에 의해서만 변경가능함
cnt2()
console.log(cnt2())

cnt3() // 지역변수 cnt 는 함수실행에 의해서만 변경가능함
cnt3()
cnt3()
console.log(cnt3())

위와 같이 지역변수 cnt 값은 오로지 함수실행(cnt1, cnt2, cnt3)에 의해서만 변경과 조회가 가능하다. 즉, 클로저는 지역변수 cnt 값에 함부로 접근하지 못하도록 보호하는 캡슐화와 은닉화 기능을 제공한다. 

 

만약 위 코드에 대하여 클로저를 사용하지 않고 동일한 기능을 하도록 만들면 어떻게 될까?

function increaseCount(cnt){
    return ++cnt
}

let cnt1 = 0
let cnt2 = 0
let cnt3 = 0

cnt1 = increaseCount(cnt1)

cnt2 = increaseCount(cnt2)
cnt2 = increaseCount(cnt2)

cnt3 = increaseCount(cnt3)
cnt3 = increaseCount(cnt3)
cnt3 = increaseCount(cnt3)

console.log(cnt1)
console.log(cnt2)
console.log(cnt3)

위 코드는 클로저를 사용하지 않고 동일한 기능을 하도록 만든 예제코드이다. 

 

클로저를 사용한 것과 비교하여 몇가지 문제점을 지니고 있다. 

let cnt1 = 0
let cnt2 = 0
let cnt3 = 0

동일한 실행환경을 만드려면 변수를 여러개 생성해야 한다. 현재는 클로저에 카운트 변수(cnt) 하나 밖에 없기 때문에 크게 와닿지는 않지만, 아래와 같이 여러개의 변수가 있다고 생각해보자!

function makeAdder(){
    let a = 0
    let b = 0
    let c = 0
    function addNums(){
        return ++a + ++b + ++c
    }
    return addNums
}

const adder1 = makeAdder()
const adder2 = makeAdder()
const adder3 = makeAdder()


console.log(adder1())
console.log(adder2())
console.log(adder3())

위 코드에서는 단순히 makeAdder 함수를 실행할때마다 a, b, c 지역변수를 저장하는 독립된 실행환경을 만들수 있다. 그러나 위 코드를 클로저를 사용하지 않고 동일한 실행환경을 여러개 만드려면 아래와 같이 수많은 변수를 사용해야 한다. 

let a1 = 0, b1 = 0, c1 = 0
let a2 = 0, b2 = 0, c2 = 0
let a3 = 0, b3 = 0, c3 = 0

function addNums(a, b, c){
    return ++a + ++b + ++c
}

console.log(addNums(a1, b1, c1))
console.log(addNums(a2, b2, c2))
console.log(addNums(a3, b3, c3))

만약 동일한 실행환경을 몇백개, 몇천개 만드려면 위 코드처럼 해서는 변수가 너무 많아진다. 

let cnt1 = 0
let cnt2 = 0
let cnt3 = 0

두번째 문제점은 클로저에서는 cnt 변수가 함수에 의해서만 변경 가능하도록 보호받았지만, 이렇게 하면 cnt1, cnt2, cnt3 는 전역변수이기 때문에 얼마든지 수정 가능하게 된다. 이는 프로그램의 오류로 이어질 수 있다. 이 부분이 클로저를 사용할때의 가장 큰 장점이다. 

 

* 클로저 활용

함수 팩토리(function factory)와 커링(Currying) - 서로 다른 기능의 함수 만들기

function multiplyXtimes(x){
    function multiply(y){
        return x * y
    }
    return multiply
}

const multiply3times = multiplyXtimes(3) 

console.log(multiply3times(4)) // 인자로 주어진 값에 3배를 반환
console.log(multiply3times(13)) // 인자로 주어진 값에 3배를 반환

const multiply5times = multiplyXtimes(5)

console.log(multiply5times(4)) // 인자로 주어진 값에 5배를 반환
console.log(multiply5times(13)) // 인자로 주어진 값에 5배를 반환

const multiply7times = multiplyXtimes(7)

console.log(multiply7times(4)) // 인자로 주어진 값에 7배를 반환
console.log(multiply7times(13)) // 인자로 주어진 값에 7배를 반환

위 코드는 클로저를 활용하여 주어진 인자 값에 다양한 배수 (3의 배수, 5의 배수, 7의 배수)를 곱하는 함수를 생성한다. 외부함수 multiplyXtimes 는 주어진 인자만큼 배수를 하는 내부함수 multiply 를 반환한다. 외부함수 multiplyXtimes 에 인자로 3을 설정하면 아래와 같이 내부함수 multiply 는 지역변수(x) 3을 저장하는 독립된 실행환경을 반환한다. 

multiply(y){
    return 3 * y
}

외부함수 multiplyXtimes 에 인자로 5를 설정하면 아래와 같이 내부함수 multiply 는 지역변수(x) 5를 저장하는 독립된 실행환경을 반환한다. 

multiply(y){
    return 5 * y
}

외부함수 multiplyXtimes 에 인자로 7을 설정하면 아래와 같이 내부함수 multiply 는 지역변수(x) 7을 저장하는 독립된 실행환경을 반환한다. 

multiply(y){
    return 7 * y
}

즉, 클로저를 활용하면 서로 다른 기능을 하는 함수를 계속 생성할 수 있다. 이를 함수 팩토리(function factory)라고 한다. 팩토리는 공장이므로 함수를 만들어내는 공장이라는 의미다.

function multiplyXtimes(x){
    function multiply(y){
        return x * y
    }
    return multiply
}


console.log(multiplyXtimes(3)(4)) // 인자로 주어진 값에 3배를 반환
console.log(multiplyXtimes(3)(13)) // 인자로 주어진 값에 3배를 반환


console.log(multiplyXtimes(5)(4)) // 인자로 주어진 값에 5배를 반환
console.log(multiplyXtimes(5)(13)) // 인자로 주어진 값에 5배를 반환


console.log(multiplyXtimes(7)(4)) // 인자로 주어진 값에 7배를 반환
console.log(multiplyXtimes(7)(13)) // 인자로 주어진 값에 7배를 반환

위 코드는 동일한 동작을 하지만 반환된 내부함수를 변수에 저장하지 않고 곧바로 함수를 실행한 것이다. 위와 같은 형태로 함수를 호출하는 것을 커링(Currying)이라고 한다. 즉, f(a, b, c) 처럼 한번에 호출하지 않고 수학의 합성함수처럼 f(a)(b)(c) 로 순차적이고 부분적으로 호출하는 형태이다. 그러므로 커링은 순차적으로 함수를 호출해야 할때나 부분적으로 함수의 일부 기능만 실행하고 싶을때 사용하면 좋다. 

 

모듈패턴 (module pattern) - 클로저를 이용하여 프라이빗 메서드(private method) 만들기

function initCount(){
    let _cnt = 0
    function _updateCount(diff){
        _cnt += diff
    }
    return {
        increaseCount(){
            _updateCount(1)
        },
        decreaseCount(){
            _updateCount(-1)
        },
        get cnt(){
            return _cnt
        }
    }
}

const counter1 = initCount() // 클로저 (독립적인 실행환경) 생성
const counter2 = initCount() // 클로저 (독립적인 실행환경) 생성
const counter3 = initCount() // 클로저 (독립적인 실행환경) 생성

counter1.increaseCount()
console.log(counter1.cnt) // 1

counter2.increaseCount()
counter2.increaseCount()
counter2.increaseCount()
counter2.decreaseCount()
console.log(counter2.cnt) // 2

counter3.decreaseCount()
counter3.decreaseCount()
counter3.decreaseCount()
counter3.decreaseCount()
counter3.increaseCount()
console.log(counter3.cnt) // -3

자바나 다른 프로그래밍 언어에서는 클래스 내부에서만 접근 가능한 프라이빗 멤버변수와 프라이빗 메서드를 설정할 수 있게 private 키워드를 제공한다. 하지만 자바스크립트에서는 기본적으로 이러한 기능을 제공하지 않는다. (물론, 자바스크립트 최신문법에서는 이러한 기능을 제공한다.)

그렇지만 자바스크립트에서는 클로저를 활용하여 프라이빗 멤버변수와 프라이빗 메서드를 구현할 수 있다. 이를 모듈패턴(module pattern)이라고 한다.  클로저 개념을 설명하는 예제코드의 카운터 예제를 클래스나 객체 형태로 만들면 위 코드와 같다.

function initCount(){
    let _cnt = 0 // 프라이빗 변수
    function _updateCount(diff){
        _cnt += diff
    }
    return {
        increaseCount(){
            _updateCount(1) // 프라이빗 함수
        },
        decreaseCount(){
            _updateCount(-1) // 프라이빗 함수
        },
        get cnt(){
            return _cnt
        }
    }
}

외부함수 initCount 를 실행하면 프라이빗 변수 _cnt 와 프라이빗 메서드 _updateCount 를 저장하는 하나의 독립된 실행환경이 만들어진다. 프라이빗 변수와 메서드는 언더바(_)를 사용하여 표시하였다. 지역변수 _cnt 와 내부함수 _updateCount 는 외부에 노출되지 않고, 오직 increaseCount, decreaseCount, cnt (getter) 에 의해서만 접근 가능하다. 

const counter1 = initCount() // 클로저 (독립적인 실행환경) 생성
const counter2 = initCount() // 클로저 (독립적인 실행환경) 생성
const counter3 = initCount() // 클로저 (독립적인 실행환경) 생성

외부함수 initCount 를 실행할때마다 카운팅(Counting) 기능을 제공하는 객체이자 클로저(독립적인 실행환경)를 여러번 생성할 수 있다. 

counter1.increaseCount()
console.log(counter1.cnt) // 1

counter2.increaseCount()
counter2.increaseCount()
counter2.increaseCount()
counter2.decreaseCount()
console.log(counter2.cnt) // 2

counter3.decreaseCount()
counter3.decreaseCount()
counter3.decreaseCount()
counter3.decreaseCount()
counter3.increaseCount()
console.log(counter3.cnt) // -3

각 실행환경은 클로저이므로 지역변수 _cnt 는 실행환경마다 다르게 변경이 가능하다. 또한 지역변수 _cnt 는 오직 메서드(increaseCount, decreaseCount, cnt 게터)에 의해서만 변경이나 조회가 가능하므로 캡슐화되어 있다. 

 

모듈패턴을 활용한 컴포넌트 생성코드

 

GitHub - sssssqew/dom-object: dom object manipulated with pure javascript

dom object manipulated with pure javascript . Contribute to sssssqew/dom-object development by creating an account on GitHub.

github.com

 

클로저의 모듈패턴을 활용한 좀 더 실용적인 예제코드는 아래와 같다. 

/*
param tag(string): html element
      attrs(object): properties of element
      values(array): child elements  
*/

//  _ 접두사 : Private 프로퍼티
var Component = (function() {
  "use strict";

  var _name = "";

  // values가 비어있다거나 아예 인자에 없다는건 추가하거나 변경하고 싶지 않고 이전 상태 그대로 두고 싶다는 의미다.
  // values 값이 하나라도 있어야 추가하거나 변경하고 싶다는 의미이므로 이때 추가하거나 변경한다.
  function _setAttributes(el, attrs) {
    if (
      attrs === undefined ||
      attrs === null ||
      Object.keys(attrs).length === 0
    )
      return;
    for (var prop in attrs) {
      el.setAttribute(prop, attrs[prop]);
    }
  }

  function _setValues(el, values) {
    if (values === undefined || values === null || values.length === 0) return;
    el.innerHTML = "";
    values.map(function(value) {
      el.append(value);
    });
  }

  var create = function(tag, attrs, values) {
    var el = document.createElement(tag);

    _setAttributes(el, attrs);
    _setValues(el, values);
    return el;
  };

  var update = function(selector, attrs, values) {
    var targetEl = document.querySelector(selector);
    if (!targetEl) return;

    _setAttributes(targetEl, attrs);
    _setValues(targetEl, values);
    return targetEl;
  };

  return {
    create,
    update,
    get name() {
      // 접근자 프로퍼티 (클로저)
      return _name;
    },
    set name(value) {
      // 접근자 프로퍼티 (클로저)
      _name = value;
    }
  };
})();

위 코드는 클로저의 모듈패턴과 즉시실행 함수를 사용하여 컴포넌트를 생성하는 하나의 간단한 라이브러리다. 컴포넌트는 웹페이지에서 특정 기능을 담당하는 일부분을 의미한다. 예를 들면,  네비게이션을 담당하는 메뉴 컴포넌트나 사용자 입력을 담당하는 로그인 컴포넌트 등이 있다. 말하자면 하나의 기능을 담당하는 HTML 덩어리다. 

var Component = (function() {}) ()

즉시실행 함수는 함수가 곧바로 호출되면서 Component 라는 하나의 클로저(독립된 실행환경)을 생성한다. 또한, 전역변수는 Component 하나 뿐이므로 Component 아래에 모든 변수와 함수들을 담은 네임스페이스(이름 공간)를 형성한다. 즉, 전역 공간을 더럽히지 않고 Component 내부의 변수와 함수는 전역변수와 충돌하지 않는다. 

var _name = "";

  _name 은 프라이빗 변수이다. 

function _setAttributes(el, attrs) {
  if (
    attrs === undefined ||
    attrs === null ||
    Object.keys(attrs).length === 0
  )
    return;
  for (var prop in attrs) {
    el.setAttribute(prop, attrs[prop]);
  }
}

_setAttributes 는 프라이빗 메서드이다. DOM 객체(el)와 속성 객체(attrs)를 이용하여 태그의 속성들을 설정한다. 

function _setValues(el, values) {
  if (values === undefined || values === null || values.length === 0) return;
  el.innerHTML = "";
  values.map(function(value) {
    el.append(value);
  });
}

_setValues 는 프라이빗 메서드이다. DOM 객체(el)와 자식요소 배열(values)을 이용하여 DOM 객체(el) 내부의 자식요소를 추가한다. 

var create = function(tag, attrs, values) {
  var el = document.createElement(tag);

  _setAttributes(el, attrs);
  _setValues(el, values);
  return el;
};

create 은 퍼블릭 메서드이다. 태그이름(tag), 속성 객체(attrs), 자식요소 배열(values)을 이용하여 DOM 객체(element)를 생성하고 속성과 자식요소를 추가한다. 속성과 자식요소 추가는 프라이빗 메서드인 _setAttributes 와 _setValues 을 사용한다. 

var update = function(selector, attrs, values) {
  var targetEl = document.querySelector(selector);
  if (!targetEl) return;

  _setAttributes(targetEl, attrs);
  _setValues(targetEl, values);
  return targetEl;
};

update 는 퍼블릭 메서드이다. 선택자(selector), 속성 객체(attrs), 자식요소 배열(values)을 이용하여 DOM 객체(element)를 업데이트한다. 속성과 자식요소 업데이트는 프라이빗 메서드인 _setAttributes 와 _setValues 을 사용한다. 

return {
  create,
  update,
  get name() {
    // 접근자 프로퍼티 (클로저)
    return _name;
  },
  set name(value) {
    // 접근자 프로퍼티 (클로저)
    _name = value;
  }
};

create, update, getter, setter 를 객체 형태로 반환함으로써 클로저 (독립된 실행환경)를 형성한다. 

 

mainDiv 라는 컴포넌트를 생성하는 전체 코드는 아래와 같다.

/*
param tag(string): html element
      attrs(object): properties of element
      values(array): child elements  
*/

//  _ 접두사 : Private 프로퍼티
var Component = (function() {
    "use strict";
  
    var _name = "";
  
    // values가 비어있다거나 아예 인자에 없다는건 추가하거나 변경하고 싶지 않고 이전 상태 그대로 두고 싶다는 의미다.
    // values 값이 하나라도 있어야 추가하거나 변경하고 싶다는 의미이므로 이때 추가하거나 변경한다.
    function _setAttributes(el, attrs) {
      if (
        attrs === undefined ||
        attrs === null ||
        Object.keys(attrs).length === 0
      )
        return;
      for (var prop in attrs) {
        el.setAttribute(prop, attrs[prop]);
      }
    }
  
    function _setValues(el, values) {
      if (values === undefined || values === null || values.length === 0) return;
      el.innerHTML = "";
      values.map(function(value) {
        el.append(value);
      });
    }
  
    var create = function(tag, attrs, values) {
      var el = document.createElement(tag);
  
      _setAttributes(el, attrs);
      _setValues(el, values);
      return el;
    };
  
    var update = function(selector, attrs, values) {
      var targetEl = document.querySelector(selector);
      if (!targetEl) return;
  
      _setAttributes(targetEl, attrs);
      _setValues(targetEl, values);
      return targetEl;
    };
  
    return {
      create,
      update,
      get name() {
        // 접근자 프로퍼티 (클로저)
        return _name;
      },
      set name(value) {
        // 접근자 프로퍼티 (클로저)
        _name = value;
      }
    };
  })();

  var mainDiv = Component.create("div", { class: "main" }, [
    Component.create("h1", { class: "title" }, ["main page"]),
    Component.create("textarea", { class: "text-area" })
  ]);

  mainDiv.name = "main pgae"; // 접근자 프로퍼티 쓰기 (데이터 캡슐화: 접근자 메서드를 통해서만 읽기, 쓰기 가능)
  console.log(mainDiv.name); // 접근자 프로퍼티 읽기

  console.log(mainDiv)

생성된 mainDiv 컴포넌트는 아래와 같다. 컴포넌트 이름과 DOM 객체가 잘 생성된 것을 확인할 수 있다. 

클로저의 모듈패턴을 활용한 컴포넌트 생성 결과

 

 

* 클로저 사용시 주의할 점

const popupBtns =  document.querySelectorAll('.popup')

function addPopupEvents(){
    for(var i=0; i<popupBtns.length; i++){
        console.log(popupBtns[i])
        popupBtns[i].onclick = function(){ // 이벤트핸들러 함수
            alert(i)
        }
    }
}

addPopupEvents()

위 코드는 클로저를 사용하여 다수의 버튼에 클릭 이벤트를 연결한다. 외부함수 addPopupEvents 에는 var 키워드로 선언된 지역변수 i 가 있다. 내부함수이자 이벤트핸들러 함수는 자신에게 존재하지 않는 i 값을 참조한다. 반복문이 실행될때마다 각 버튼에는 이벤트핸들러 함수가 등록되면서 지역변수 i 값을 저장하는 각각의 클로저를 생성한다. 

하지만 한가지 문제가 있다. 실제로 각 버튼을 클릭해보면 서로 다른 인덱스 i 값이 출력되지 않고 3이라는 숫자만 출력된다. 왜냐하면 각 클로저(3개의 실행환경)는 지역변수 i 값을 공유하고 있는데 반복문이 종료되고 나면 var 키워드에 의하여 지역변수 i 값은 3이 되기 때문이다. 사용자가 버튼을 클릭하는 시점은 반복문 종료 이후이므로 지역변수 i 값은 모두 3이 출력된다.

const popupBtns =  document.querySelectorAll('.popup')

function addPopupEvents(){
    for(let i=0; i<popupBtns.length; i++){
        console.log(popupBtns[i])
        popupBtns[i].onclick = function(){
            alert(i)
        }
    }
}

addPopupEvents()

해결방법은 위와 같이 var 키워드 대신 let 키워드를 사용하면 된다. let 키워드는 블록 스코프이므로 반복문이 실행될때마다 새로운 메모리 주소에 지역변수 i 값을 생성한다. 그러므로 클로저가 생성되면서 지역변수 i 값은 공유되지 않고 각각 다른 메모리 주소에 존재한다. 그래서 실제로 버튼을 클릭해보면 서로 다른 인덱스 값이 출력된다. 

const popupBtns =  document.querySelectorAll('.popup')

function addPopupEvents(){
    popupBtns.forEach( (btn, i) => {
        console.log(btn)
        btn.onclick = function(){
            alert(i)
        }
    })
}

addPopupEvents()

다른 해결방법은 위와 같이 forEach 메서드를 활용하는 것이다. 이 방법은 let 키워드를 사용하는 방법과 동일한 결과를 출력한다. 

 

* 즉시실행함수 (mmediately-invoked function expression)

const fruits = ["apple", "banana", "orange"]

// 라이브러리 (underscore.js)
const _ = (function(){
    const _ = {}
    _.getFruits = function(){
        return this._fruits
    }
    _.setFruits = function(fruits){
        this._fruits = fruits
    }

    return _
})()

_.setFruits(fruits)
console.log(_.getFruits()) 



const products = ["paper", "cosmetic", "coke"]

const _ = {}
_.getProducts = function(){
    return this._products
}
_.setProducts = function(products){
    this._products = products
}


_.setProducts(products)
console.log(_.getProducts())

즉시실행함수는 함수선언과 동시에 곧바로 실행이 되는 함수이다. 주로 라이브러리를 만들때 사용이 된다. underscore.js 라는 라이브러리는 언더바(_)를 전역변수로 사용한다. 위 코드를 살펴보면 한가지 문제점이 있다. 라이브러리가 이미 언더바(_)를 전역변수로 사용중인데 프로그램 구현시 언더바(_)를 사용하고 싶다. 이러한 경우에 위 코드는 오류를 발생시킨다.

해결방법은 아래와 같이 즉시실행함수로 프로그램 전체를 감싸주면 변수충돌을 막고 캡슐화 할 수 있다.

const fruits = ["apple", "banana", "orange"]

// 라이브러리 (underscore.js)
const _ = (function(){
    const _ = {}
    _.getFruits = function(){
        return this._fruits
    }
    _.setFruits = function(fruits){
        this._fruits = fruits
    }

    return _
})()

_.setFruits(fruits)
console.log(_.getFruits()) 

const products = ["paper", "cosmetic", "coke"]

const $ = (function(){
    const _ = {}
    _.getProducts = function(){
        return this._products
    }
    _.setProducts = function(products){
        this._products = products
    }
    return _
})()

$.setProducts(products)
console.log($.getProducts())

 비록 전역범위에서는 달러($)를 사용하지만, 내부 프로그램 전체에서 언더바(_)를 사용할 수 있다. 그리고 라이브러리(underscore.js) 의 언더바(_)와 충돌하지 않는다. 이렇게 즉시실행함수는 변수충돌을 막고 프로그램을 캡슐화할때 이롭게 사용하면 좋다. 

 

* 재귀함수와 메모이제이션 (memoization)

function factorial(n){
    console.log('fact !', n)
    if(n === 0 || n === 1) return 1
    else return n * factorial(n-1)
}

console.log(factorial(3) + factorial(6))

위 코드는 재귀함수를 이용하여 팩토리얼을 계산한다. 그런데 한가지 문제점이 있다. factorial(6) 을 계산할때 다시 factorial(3) 에 대한 연산을 중복으로 하게 된다. 

재귀함수를 이용한 팩토리얼 계산 - 연산 일부 중복

 

function factorial(n){
    if(factorial[n]) return factorial[n] // 이미 계산된 결과가 있으면 저장해 놓은 값을 사용함

    console.log('fact !', n)
    if(n === 0 || n === 1) return 1
    else{
        factorial[n] = n * factorial(n-1) // 계산된 중간결과를 저장해 놓음
        return factorial[n]
    }
}

console.log(factorial(3) + factorial(6))

위 코드는 계산결과를 반환하기 전에 함수의 프로퍼티에 결과값을 저장해둔다. 함수 실행 초반에 이미 계산된 결과가 있으면 다시 계산하지 않고 함수의 프로퍼티에 저장해 놓은 값을 그대로 사용한다. 이렇게 하면 연산횟수를 줄일수 있다. 이를 기억한다는 의미에서 메모이제이션이라고 한다. 

메모이제이션을 이용한 팩토리얼 계산

 

* 동적으로 함수의 this 값 변경하기 - call, apply, bind 

function getInfo(){
    console.log(this) // 윈도우 객체
}
getInfo()

function 키워드로 선언한 함수는 기본적으로 this 값을 가지고 있다. 브라우저에서는 윈도우 객체를 가리킨다. 

function getInfo(){
    console.log(this)
}

const sunrise = {
    name: 'sunrise',
    age: 23,
    city: "daegu"
}

const victoria = {
    name: 'victoria',
    age: 17,
    city: 'seoul'
}

getInfo.call(sunrise)
getInfo.call(victoria)

위 코드는 두개의 객체를 생성하고 함수의 call 메서드를 이용하여 함수내에 동적으로 this 값을 주입한다. 이를 this 값을 바인딩(binding) 한다고 한다. call 메서드의 첫번째 인자로 바인딩하고 싶은 값을 넘겨주면 된다. this 값의 출력결과는 아래와 같이 call 메서드에 전달한 인자(객체)에 따라 다르다. 

함수의 call 메서드를 사용한 this 값의 동적인 변경

function getInfo(){
    console.log(this)
}

const sunrise = {
    name: 'sunrise',
    age: 23,
    city: "daegu"
}

const victoria = {
    name: 'victoria',
    age: 17,
    city: 'seoul'
}

getInfo.apply(sunrise)
getInfo.apply(victoria)

apply 메서드를 사용해도 아래와 같이 동일한 결과를 출력한다.

함수의 call 메서드를 사용한 this 값의 동적인 변경

function getInfo(friend1, friend2){
    console.log(this)
    console.log(friend1, friend2)
}

const sunrise = {
    name: 'sunrise',
    age: 23,
    city: "daegu"
}

const victoria = {
    name: 'victoria',
    age: 17,
    city: 'seoul'
}

getInfo.call(sunrise, '영희', '철수')
getInfo.call(victoria, '영희', '철수')

getInfo.apply(sunrise, ['영희', '철수'])
getInfo.apply(victoria, ['영희', '철수'])

 call 과 apply 메서드의 차이점은 인자를 전달하는 방식에 있다. apply 메서드는 첫번째 인자를 제외하고 배열에 인자를 담아서 전달해야 한다. 

함수의 call, bind 메서드를 사용한 this 값 바인딩 결과 화면&amp;amp;nbsp;

function getInfo(friend1, friend2){
    console.log(this)
    console.log(friend1, friend2)
}

const sunrise = {
    name: 'sunrise',
    age: 23,
    city: "daegu"
}

const victoria = {
    name: 'victoria',
    age: 17,
    city: 'seoul'
}

bindedGetInfo1 = getInfo.bind(sunrise)
bindedGetInfo2 = getInfo.bind(victoria)

bindedGetInfo1('영희', '철수')
bindedGetInfo2('영희', '철수')

bind 메서드는 call, apply 메서드와 사용법에 조금 차이가 있다. call, apply 메서드는 메서드 실행과 동시에 this 값을 바인딩하고 getInfo 함수를 실행하지만, bind 메서드는 실행하면 this 값만 바인딩하고 getInfo 함수는 실행되지 않는다. bind 메서드는 실행후 반환된 함수 (this 값이 바인딩된 함수)를 실행하면 getInfo 함수가 실행된다. 결과는 동일하다. 

const getInfo = (friend1, friend2) => {
    console.log(this)
    console.log(friend1, friend2)
}

const sunrise = {
    name: 'sunrise',
    age: 23,
    city: "daegu"
}

const victoria = {
    name: 'victoria',
    age: 17,
    city: 'seoul'
}

getInfo.call(sunrise, '영희', '철수')
getInfo.call(victoria, '영희', '철수')

화살표 함수는 자체적인 this 값이 존재하지 않기 때문에 위 코드와 같이 call 메서드로 this 값을 바인딩해주더라도 동작하지 않고, 상위 스코프의 this 값을 가리킨다. 즉, 현재 함수가 선언된 위치에서 상위 스코프는 전역이므로 전역 스코프의 this 값인 윈도우 객체를 출력한다.

화살표 함수의 this 값 출력 결과

 

 

 

* 연습과제 1

아래는 함수 스코프 수업 예제를 변경하여 출력 결과를 다르게 한 것이다. 아래와 같은 출력 결과가 나오도록 함수 스코프 예제를 변경해보자!

const fruit = 'apple'
let printB = null

// 구현하기


printA() // banana
printB() // banana

함수 스코프 출력 결과

 

* 연습과제 2

다음은 나의 SNS 친구목록이다. 아래 코드의 filter 함수를 구현하여 캡쳐화면의 결과처럼 해당목록에서 서울에 사는 친구들만 추출해보자! filter 함수의 첫번째 인자에는 filterSeoul 이라는 콜백함수가 들어가고, 두번째 인자에는 친구목록이 들어간다. filter 함수는 조건에 맞는 배열요소들만 추출된 새로운 배열을 반환해야 한다. 

const friends = [
    {name: 'victoria', age: 13, city: 'seoul'},
    {name: 'sun', age: 34, city: 'busan'},
    {name: 'johseb', age: 25, city: 'busan'},
    {name: 'syleemomo', age: 9, city: 'seoul'},
    {name: 'hannah', age: 41, city: 'daegu'},
    {name: 'shara', age: 37, city: 'seoul'},
    {name: 'martin', age: 28, city: 'daegu'},
    {name: 'gorgia', age: 39, city: 'seoul'},
    {name: 'nana', age: 24, city: 'busan'},
    {name: 'dannel', age: 19, city: 'seoul'},
]

const seoulFriends = filter(filterSeoul, friends)
console.log(seoulFriends)

filter 함수에 의하여 서울에 사는 친구들만 추출한 결과 화면

 

* 연습과제 3

아래 코드는 계산기 예제에서 pow 라는 기능을 추가하는 중이다. pow 함수가 동작하도록 코드를 완성해보자! pow(2, 3)은 2의 3제곱이고, pow(3, 4)는 3의 4제곱이다. 

function add(a, b){
    return a + b
}
function subtract(a, b){
    return a - b
}
function multiply(a, b){
    return a * b
}
function divider(a, b){
    return a / b
}
function pow(a, b){
    // 구현하기
}

function calculator(mode, a, b, ...funcs){
    let ret = null
    switch(mode){
        case '+':
            ret = funcs[0](a, b)
            break
        case '-':
            ret = funcs[1](a, b)
            break
        case '*':
            ret = funcs[2](a, b)
            break
        case '/':
            ret= funcs[3](a, b)
            break
        case '^':
            ret = funcs[4](a, b)
            break
    }
    return ret
}

// 테스트 케이스
const ret1 = calculator('+', 3, 4, add, subtract, multiply, divider, pow)
const ret2 = calculator('-', 3, 4, add, subtract, multiply, divider, pow)
const ret3 = calculator('*', 3, 4, add, subtract, multiply, divider, pow)
const ret4 = calculator('/', 3, 4, add, subtract, multiply, divider, pow)
const ret5 = calculator('^', 3, 4, add, subtract, multiply, divider, pow)

console.log(ret1)
console.log(ret2)
console.log(ret3)
console.log(ret4)
console.log(ret5)

pow 기능이 추가된 계산기 결과 화면

 

* 연습과제 4

아래는 댓글에서 비속어가 포함된 단어들을 걸러내는 필터링 기능의 코드이다. 아래 코드와 동일한 결과가 출력되도록 클로저를 활용하여 다시 작성해보자!

const comment = '너는 진짜 못말리는 바보 똥개 그지다 !'
const insults = ['바보', '똥개', '그지']

function separateStr(str, delimeter){
    return str.split(delimeter)    
}
function filterKeyword(arr, keyword){
    return arr.filter(word => !word.includes(keyword))
}
let strSeparated = separateStr(comment, ' ')

for(let insult of insults){
    strSeparated = filterKeyword(strSeparated, insult)
}
console.log(strSeparated)

댓글 필터링 결과 화면

 

* 연습과제 5

아래 코드는 배열에서 특정 배열요소가 포함되어 있는지 검색하는 기능이다. 아래 코드와 동일한 결과가 출력되도록 클로저를 활용하여 다시 작성해보자!

const animals = ['cat', 'lion', 'turtle', 'dog', 'pig']
const fruits = ['apple', 'banana', 'strawberry', 'pineapple', 'pear']

function searchItemInCategory(category, key){
    return category.filter(item => item === key)[0]
}

console.log(searchItemInCategory(animals, 'turtle')) // searchItemInCategory 내부의 category 변수에는 접근하지는 못하지만 외부인자에 의하여 수정이 가능함
console.log(searchItemInCategory(animals, 'pig'))
console.log(searchItemInCategory(animals, 'banana'))

console.log(searchItemInCategory(fruits, 'strawberry'))
console.log(searchItemInCategory(fruits, 'apple'))
console.log(searchItemInCategory(fruits, 'lion'))

배열요소 검색 결과 화면

 

* 연습과제 6

아래는 생성자 함수와 프로토타입을 이용하여 객체를 생성하는 코드이다. 아래와 동일한 결과를 출력하도록 클로저와 모듈패턴을 활용하여 다시 작성해보자!

const friends = [
    {name: 'victoria', age: 13, city: 'seoul'},
    {name: 'sun', age: 34, city: 'busan'},
    {name: 'johseb', age: 25, city: 'busan'},
    {name: 'syleemomo', age: 9, city: 'seoul'},
    {name: 'hannah', age: 41, city: 'daegu'},
    {name: 'shara', age: 37, city: 'seoul'},
    {name: 'martin', age: 28, city: 'daegu'},
    {name: 'gorgia', age: 39, city: 'seoul'},
    {name: 'nana', age: 24, city: 'busan'},
    {name: 'dannel', age: 19, city: 'seoul'},
]

function Person(name, age, city, friends){
    this.name = name
    this.age = age
    this.city = city

    // 초기값이 배열이나 객체인 경우 깊은복사로 저장 및 조회하기
    let _friends = JSON.parse(JSON.stringify(friends)) // 전역변수 friends 는 참조만 하고 프라이빗 변수 _friends 는 외부에서 변경하지 못하도록 깊은복사로 저장함

    this.getFriends = function(){
        return JSON.parse(JSON.stringify(_friends)) // 프라이빗 변수 _friends 를 보호하기 위하여 깊은복사로 조회함
    }

}
Person.prototype = {
    get friends(){
        return this.getFriends() 
    },
    filterFriendsInMyCity(){ 
        return this.friends.filter(friend => friend.city === this.city) 
    },
}


const person = new Person('영희', 23, 'daegu', friends)

console.log(person.friends === friends) // 전역변수 friends 와 프라이빗변수 _friends 주소가 다름을 확인함 (복사본을 저장하기 때문)
person.friends[0].name =  "태양" // 프라이빗 변수 _friends 를 변경하지 못함 (복사본을 조회하기 때문)

console.log(person.friends) 
console.log(person.filterFriendsInMyCity())

const person2 = new Person('철수', 35, 'seoul', friends)
console.log(person2.friends) 
console.log(person2.filterFriendsInMyCity())

친구목록 필터링 결과 화면

 

연습과제 7

재귀함수를 사용하여 아래와 같이 중첩된 객체에서 함수의 인자로 주어진 프로퍼티값을 검색하고, 프로퍼티가 존재하면 해당 프로퍼티의 값을 출력한다. 이때 동일한 프로퍼티명이 여러군데 존재하면 세번째 인자에 뎁스(depth)를 설정하여 특정 깊이에 존재하는 객체의 프로퍼티 값을 출력하도록 한다. (필요시 클로저도 함께 사용하도록 한다.)

const metadata = {
  title: "Scratchpad",
  translations: {
    info: {
      locale: "de",
      localization_tags: [],
      last_edit: "2014-04-14T08:43:37",
      url: "/de/docs/Tools/Scratchpad",
      title: "JavaScript-Umgebung",
      time: {
        hour: 4
      }
    },
  },
  url: "/ko/docs/Tools/Scratchpad",
}
console.log(findKeyOfObj("title", metadata, 1))

뎁스 1의 "title" 값은 아래와 같다. 

console.log(findKeyOfObj("title", metadata, 3))

뎁스 3의 "title" 값은 아래와 같다. 

console.log(findKeyOfObj("hour", metadata, 4))

뎁스 4의 "hour"값은 아래와 같다. 

 

 

 

 

 

함수 선언과 함수참조 그리고 함수 실행의 차이 (메모리에서 일어나는 일)

함수입력(주소참조 & 값 참조)

함수 콜스택과 이벤트 루프 - 비동기 주제인데 따로 뺄까?

 

 

 

728x90