본문 바로가기
  • soldonii's devlog
Javascript 공부/Zero To Mastery(-)

(11) 자바스크립트 심화 2 - 클로져, 커링, 배열 메소드 등

by soldonii 2019. 8. 27.

*Udemy의 "The Complete Web Developer in 2019 : Zero To Mastery" 강의에서 학습한 내용을 정리한 포스팅입니다.

*https://soldonii.github.io에서 2019년 7월 16일(화)에 작성한 글을 티스토리로 옮겨온 포스팅입니다.

*자바스크립트를 배우는 단계라 오류가 있을 수 있습니다. 틀린 내용은 댓글로 말씀해주시면 수정하겠습니다. 감사합니다. :)


1. Advanced Functions

변수를 global scope에서 선언하는 것은 안전하지 못한 방법이다. 이러한 bug를 방지하기 위해서 함수 내에 변수를 선언하고, 함수 실행시마다 해당 변수가 초기화되는 방법을 사용할 수 있다.

const first = () => {
  const greet = "Hi";
  const second = () => {
    alert(greet);
  }
  return second;
}

// Closures
const newFunc = first();
newFunc();

위 사례에서 변수 greet 을 함수 first 내에 선언했다. 이 함수가 실행될 때마다 greet 은 “Hi”라는 값으로 초기화된다. 따라서 함수 외부에 greet 이라는 변수가 있더라도, 개발자가 변수 greet 에 원하는 값으로 계속 초기화되므로 예상치 못한 버그를 예방할 수 있다. 변수를 global variable이 아니라 function scope 내의 local variable로 처리하는 방식이다.

 

 

# 클로저(Closures)

the function is exectued and it’s never going to run again. BUT it’s going to remember that there are refrences to those variables. so the child scope always has access to parent scope.

클로저의 핵심은, 자식 scope는 항상 부모 scope에 대한 접근권한을 가지고 있다는 것이다. 사례를 통해 살펴보자.

const first = () => {
  const greet = "Hi";
  const second = () => {
    alert(greet);
  }
  return second;
}

// Closures
const newFunc = first(); // first() 함수는 한 번만 실행된다.
newFunc(); // second()와 같은 의미인데, seconde 함수 내부에서는 greet 변수가 선언되지 않았다.
           // 그럼에도 불구하고, 선언된 당시의 변수환경을 기억하기 때문에, closure를 이용해서
           // 상위 scope(first 함수의 scope)에 존재하는 greet에 접근할 수 있다.

 

위 사례에서 함수 first 는 단 한번만 실행됐으며, 실행의 결과값을 변수 newFunc 에 담은 상태이다. 여기서 1번 방식과 2번 방식은 동일한 의미인데, 의문이 생긴다. 2번 방식을 보면, 변수 newFunc 에 담긴 함수 second 를 실행하기 위해서는 변수 greet 을 알아야 하는데, 함수 second 내부에 변수 greet 은 선언되지 않았다. 따라서 greet 을 referencing할 수 없는 것이 아닐까?

 

하지만 Javascript에서 child scope는 언제나 그들이 parent scope에 대한 access가 가능하다. 따라서 child scope에 해당되는 함수 second 는 parent scope인 함수 first 에 접근할 수 있으며, 함수 first 내에서 변수 greet 이 선언되어 있으므로 해당 값을 가져와서 출력할 수 있다.

 

반대로 parent scope는 child scope에 대한 접근 권한이 없다.

 

# 커링(Currying)

the process of converting a function that takes multiple arguments into a function that takes them one at a time.

커링은 parameter가 n개인 함수를 호출할 때, 하나의 함수가 n개의 인자를 받는 대신 n개의 함수가 각각 1개씩 인자를 받도록 하는 함수형 프로그래밍(functional programming)의 기법 중 하나이다. 왜 이런 불필요해 보이는 일을 할까?

함수를 자유자재로 customizing할 수 있으며, 반복적으로 사용되는 매개변수를 내부적으로 저장하여 매번 인자를 전달하지 않아도 원본 함수가 기대하는 기능을 채워 넣을 수 있기 때문이다. 자세한 것은 아래 내가 공부하면서 참고한 링크를 통해 확인 가능하다.

 

 [번역] 초보자를 위한 함수형 자바스크립트 Currying 가이드

 Javascript에서 커링 Currying 함수 작성하기

const multiply = (a, b) => a * b;
const curriedMultiply = (a) => (b) => a * b;
curriedMultiply(3)(5); // 15

const multiplyBy5 = curriedMultiply(5);
multiplyBy5(6); // 30

 

# Compose

the act of putting two functions together to form a third function where the output of one function is the input of the other.
const compose = (f, g) => (a) => f(g(a));

const sum = (num) => num + 1;
compose(sum, sum)(5); // 7

 

# Avoiding Side Effect, function purity.

1. Avoiding Side Effect : 함수는 하나의 소우주(small universe)이다. 이 함수에서 발생하는 일이 함수 외부, 즉 outside world에 어떠한 영향도 미치지 않도록 최선의 노력을 해야 한다. (ex. 함수 내부에서 선언한 변수 때문에 함수 외부의 변수가 변경되는 일이 발생하지 않도록..) 

출처 : https://www.udemy.com/the-complete-web-developer-zero-to-mastery/

2. Function Purity : 함수를 Deterministic하게, 즉 input이 동일하다면 언제나 output도 동일하도록 만들어야 한다. 

출처 : https://www.udemy.com/the-complete-web-developer-zero-to-mastery/

 

2. Advanced Arrays

# .map

.map 은 배열 내 요소를 돌면서 새로운 배열을 반환한다. 빈 배열을 만들고 .forEach와 push를 사용해서 새로운 배열을 만드는 것도 가능하다. 하지만 .forEach는 ‘Avoiding Side Effect’가 어렵다. 반면 .map을 사용하면 같은 input이면 같은 output을 return하기 때문에 ‘function purity’의 개념에 더 적합하다. 또한 .map 은 원본 배열을 변형하지 않는다는 장점 또한 가지고 있다.

const array = [1, 2, 10, 16];
const mapArray = array.map(num => num * 2); 
console.log(mapArray); // [2, 4, 20, 32]

 

# .filter

.filter 는 배열 내 요소를 돌면서 조건에 부합하는 결과만을 새로운 배열에 담아서 반환한다.

const array = [1, 2, 10, 16];
const filterArray = array.filter(num => num > 5);
console.log(filterArray); // [10, 16]

 

# .reduce

.reduce 는 배열 내 요소를 돌면서 return에서 지정한 결과값을 accumulator에 누적하여 담아 최종 acc 값을 return한다.

const array = [1, 2, 10, 16];
const reducedArray = array.reduce((acc, num) => acc + num, 0);
console.log(reducedArray); // 29

 

3. Advanced Objects

 Javascript Object Explorer : object의 다양한 활용 방법에 대해 play해볼 수 있는 사이트

 

# reference type

출처 : https://www.udemy.com/the-complete-web-developer-zero-to-mastery/

1. primitive type : number, string, boolean, undefined, null 등 javascript가 자체적으로 정해놓은 data type이다.

2. reference type : 프로그래머가 만들고 정의한 data type이다.

출처 : https://www.udemy.com/the-complete-web-developer-zero-to-mastery/

 

첫번째 사진은 두번째 사진과 동일한 의미이다. 즉 object1  {value:10} 이 담겨있는 주소인 box1을 의미하고, object2  object1 을 referencing하고 있다. object3 도 마찬가지로 box3을 의미한다. 값 자체가 아니라 주소를 referencing 하고 있다는 점이 중요하다.

 

# context vs. scope

scope : let, const는 {} 중괄호가 있을 때, var는 함수가 정의될 때 새로운 scope가 발생한다.

context : 현재 어떤 객체에 속해있는지를 지칭하는 개념이다. this 키워드와 관련있다.

function b() {
  let a = 4;
}
console.log(a); // referenceError. a는 function scope 내에서만 존재한다.

 

# instantiation

Instantiation은 객체를 그대로 사용하지 않고 복사하여 사용하는 것을 의미한다. 사례로 살펴보자.

class Player {
  constructor(name, type) {
    this.name = name;
    this.type = type;
  }
  introduce() {
    console.log(`Hi I am ${this.name}, I'm a ${this.type}.`)
  }
}

let player1 = new Player("hyunsol", "troll");
/* 
player1
Player {name: "hyunsol", type: "troll"}
name: "hyunsol"
type: "troll"
__proto__: Object 
*/

 

class 를 사용하여 Player를 복사하면 가장 먼저 constructor 함수가 실행된다. constructor 함수는 name, type property를 새로운 player1 객체에 생성한다. 새롭게 생성된 객체 player1 은 고유의 name, type 을 가지고 있다.

class Player {
  constructor(name, type) {
		console.log(this); // Wizard{}
    this.name = name;
    this.type = type;
  }
  introduce() {
    console.log(`Hi I am ${this.name}, I'm a ${this.type}.`)
  }
}

class Wizard extends Player {
  constructor(name, type) {
    super(name, type)
  }
  play() {
    console.log(`WEEEEEE I'm a ${this.type}`);
  }
}

const wizard1 = new Wizard("Shelly", "Healer");

 

위 코드를 한 줄 한 줄 해석해보면 다음과 같다.

  • class Wizard extends Player : Wizard 객체가 Player 객체에 존재하는 prototype 객체를 상속받겠다는 의미이다.
  • super : extends 를 사용할 때, 가져올 대상이 되는 클래스의 값을 불러오기 위해서는 super 키워드가 필요하다. 즉 super(name, type) 은 Wizard 클래스가 Player 클래스에서 this.name = name, this.type = type 을 가져오겠다는 의미이다.
  • new : Wizard class를 통해서 새로운 객체를 만들도록 한다. const wizard1 = new Wizard("Shelly", "Healer"); 를 해석해보자.
    1. “Shelly”, “Healer”를 인자로 받아서 새로운 wizard1이라는 객체를 만들고자 한다. wizard1 객체를 만들 때는 Wizard 클래스를 참고할 것이고, Wizard 클래스의 값을 복사해서 만들 것이다.
    2. Wizard 클래스 내부에서 constructor 메소드가 실행이 되고, super에 의해서 Player 클래스 내부의 constructor 메소드에 접근해서 이를 실행시킨다.
    3. Player 클래스의 constructor 를 통해, 입력받은 인자(“Shelly”, “Healer”)가 wizard1 객체의 value로 생성이 된다.

출처 : https://www.udemy.com/the-complete-web-developer-zero-to-mastery/

 

4. Pass by Value vs. Pass by Reference

출처 : https://www.udemy.com/the-complete-web-developer-zero-to-mastery/

 

# Pass by Value

Primitive Type은 IMMUTABLE하다. 즉, 해당 값을 바꾸는 것은 불가능하고, 만약 다른 값을 원하면 메모리에서 값을 지운 후 재할당하는 과정을 거쳐야 한다.

let a = 5;
let b = 10;

 

a에는 5가, b에는 10이 할당되어 있고, 각 변수는 메모리 어딘가에 할당된 값을 저장해놓고 있다. a b에 대해서, b a에 대해서 존재 유무를 모르고 있다. 이러한 방식을 pass by value라고 한다.

let a = 5;
let b = a;
b++;

console.log(a); // 5;
console.log(b); // 6;

 

위 경우에 b  a 의 값을 그대로 b 의 값으로 복사한 후, 다른 메모리 공간에 저장한 상태이다. 변수의 데이터 타입이 Primitive Type 중 하나인 number이기 때문에 a  b 사이에는 아무런 연관이 없다. 단지 b  a 의 값을 복사해서 자신만의 메모리 공간에 저장해 놓았을 뿐이다.

 

# Pass by Reference

반면 object는 pass by reference에 해당된다.

let obj1 = {name: "Yao", password: '123'};
let obj2 = obj1;

console.log(obj1); // {name : "Yao", password : '123'}
console.log(obj2); // {name : "Yao", password : '123'}

obj2.password = 'easypeasy';

console.log(obj1); // {name : "Yao", password : 'easypeasy'}
console.log(obj2); // {name : "Yao", password : 'easypeasy'}

 

obj1과 obj2에 객체를 할당하는 행위는, 각 변수에 “얘들아! 너네가 찾는 객체는 이 주소에 있어!”라고 말하는 것과 같다. 메모리 어딘가에 저장되어 있는 객체를 pointing하고 있는 것이다. 그리고 let obj2 = obj1; 는 obj2에게 obj1이 알고 있는 ‘{name: “Yao”, password: ‘123’}’이 보관되어 있는 메모리 주소를 알려주는 것과 같은 행위이다. 더 쉬운 이해를 위해 사람에게 대입하여 이해해보자.

  • obj1이 나, obj2가 내 여친이라고 가정하고 ‘{name: “Yao”, password: ‘123’}’을 내가 작성한 우리 커플의 데이트 일지라고 생각해보자.
  • 나는 여친에게 우리 데이트 일지를 내 방 책상 첫번째 서랍에 보관해 놓았다고 알려줬다. (let obj2 = obj1;)
  • 내가 데이트 일지 내용을 일부 잘못 작성했는데, 내가 시간이 없어서 여친이 대신 수정해주려고 한다.(obj2.password = 'easypeasy';)
  • 여친은 어디에 보관되어 있는지 장소를 알고 있기 때문에, 그 장소에 가서 데이트 일지를 수정했다.
  • 따라서 데이트 일지는 내가 보아도(console.log(obj1);), 여친이 보아도(console.log(obj2);) 변경된 내용을 같이 볼 수 밖에 없다.

좀 어처구니 없어 보이는 비유이기는 하지만,, 나는 이렇게 이해하니 단박에 이해가 잘 되었다..ㅎㅎ

 

pass by reference의 장점과 단점
  • 장점 : 한 곳에만 값을 저장해 놓음으로써, 우리는 space와 memory를 절약할 수 있다.
  • 단점 : 실수에 의해 누군가가 referencing할 원본 object를 훼손할 수 있다.

그렇다면 원본을 복사하면 단점을 방지할 수 있을까? 우선 아래 방식으로 원본을 복사할 수 있다.

// 1. copy array
let arr = [1, 2, 3, 4];
let arr2 = [].concat(arr);

// 2. copy object
let obj1 = {a: 'a', b: 'b', c: 'c'};
let clone1 = Object.assign({}, obj1); 
let clone2 = {...obj}; // clone1 방식, clone2 방식 모두 가능하다.

 

그런데 만약 객체 내의 value가 또 다른 객체라면 어떻게 될까?

let obj = {
  a: 'a',
  b: 'b',
  c: {
    deep : "try and copy me"
  }
};

let clone1 = Object.assign({}, obj);
let clone2 = {...obj};
let superClone = JSON.parse(JSON.stringify(obj));

obj.c.deep = 'hahaha';
console.log(obj); // { a: 'a', b: 'b', c: {deep : 'hahaha'}}
console.log(clone1); // { a: 'a', b: 'b', c: {deep : 'hahaha'}}
console.log(clone2); // { a: 'a', b: 'b', c: {deep : 'hahaha'}}
console.log(superClone); // { a: 'a', b: 'b', c: {deep : 'try and copy me'}}

 

위 사례를 살펴보면, Object.assign(또는 {…obj}) 를 활용해서 원본 객체를 복사했으나, 객체 내의 또 다른 객체(c)는 여전히 pass by reference가 유효하다. 이는 우리가 원하는 것이 아니다! 이 코드에서 우리가 취한 방식은 ‘shallow copy’, 즉 1단계(1st layer)만 복사한 것이다. deep copy를 진행하려면 어떻게 해야할까?

JSON을 활용하여 객체에 대해서 ‘deep copy’를 진행할 수 있다.(let superClone = JSON.parse(JSON.stringify(obj));)

  • JSON.stringify(obj) : obj를 모두 문자열로 변환해준다.
  • JSON.parse(str) : str을 모두 객체로 변환해준다.
  • 그러나 만약 원본 객체가 매우 거대하다면, JSON을 활용하여 deep copy를 하는 것은 많은 시간이 소요될 가능성이 높다. 따라서 field에서 자주 보기는 힘들 것이지만, 이러한 방식도 있다는 것을 알아두자.

참고 링크

5. Type Coercion

operator의 왼쪽과 오른쪽에 등장하는 것이 서로 다른 type일 경우, 서로 같은 type이 되도록 둘 중 하나가 Javascript engine에 의해 변형되는 것을 의미한다.

사실 Type Coercion의 결론은,, 절대로==를 쓰지말고===를 쓰라는 것이다! Javascript에서는 == 를 사용하면 Type Coercion이 발생하며, 이는 원치 않는 결과를 초래할 수 있기 때문이다.

 

1 == '1' // true

 

참고로 Type Coercion은 조건문에서도 발생한다.

if (1) { // 1을 true로 type coerce한다.(0은 false로 변환한다.)
  console.log(5);
}

 

# Object.is

-0 === +0; // true
Object.is(-0, +0); // false

 mdn equality comparison and sameness

댓글