컴퓨터 공부/🕸️ Web

Javascript 중급 - 2) closure, 구조 분해 할당, map, filter, reduce, 전개연산자

letzgorats 2023. 12. 8. 19:59

< Closure >

closure 에 대한 설명

- 클로저(Closure)는 함수와 그 함수가 선언된 어휘적 환경(Lexical Environment)의 조합을 말한다. 다시 말해서, 클로저는 내부 함수에서 외부 함수의 스코프(Scope)에 접근할 수 있게 해주는 기능이다. 자바스크립트에서는 함수가 생성될 때마다, 즉 함수 생성 시에 클로저가 만들어진다.

 

다른 함수 내부에 정의된 함수(innerFunction)가 있는 경우 외부 함수(outerFunction)가 실행을 완료하고 해당 변수가 해당 함수 외부에서 더 이상 엑세스할 수 없는 경우에도 해당 내부 함수는 외부 함수의 변수 및 범위에 액세스할 수 있다.

...

무슨말이죠 이게?

...

예를 들어, 외부 함수에서 변수를 선언하고, 내부 함수에서 이 변수에 접근할 수 있게 해주는 것이 클로저의 한 예시인데, 이렇게 클로저를 사용하면 내부 함수는 외부 함수가 종료된 후에도 외부 함수의 변수에 접근할 수 있다는 말이다.

아래 예시를 살펴보자.

// What is closure?
function outerFunction(outerVariable) {
    return function innerFunction(innerVariable) {
        console.log('Outer Variable: ' + outerVariable);
        console.log('Inner Variable: ' + innerVariable);
    }
}

const newFunction = outerFunction('outside');
newFunction('inside');

 

1. outerFunction('outside')는 변수 newFunction 에 할당되는 즉시 호출된다.

 

2. 호출되면 outerFunction 은 변수 newFunction 을 outerFunction(outerVariable) 대신 innerFunction(innerVariable) 을 반환한다. (newFunction은 함수를 리턴하는데, innerFunction을 return 한다는 말)

 

3. 그런 다음 변수를 newFunction('inside')으로 호출하여 innerFunction('inside')을 호출한다.

(현재 newFunction은 innerFunction이므로)

여기서 클로저는 innerFunction이 원래 outerFunction('outside')으로 설정한 outerVariable 매개변수를 기억하고 액세스할 수 있다는 것이다. 따라서, 'inside' 로만 호출되었더라도 'outside'와 'inside'를 모두 console.log 할 수 있다.

outside 변수도 클로저가 기억

 

다른 예시를 한 번 또 보겠습니다.

let a = 'a';
function functionB() {
    let c = 'c';
    console.log(a, b, c);
}

function functionA() {
    let b = 'b';
    console.log(a, b);
    functionB();
}

functionA();

 

이 코드를 실행해보면, 아래와 같이 'b' is not defined 라는 참조에러가 나온다.

'b' is not defined

 

위 코드에서 어떤 부분이 문제여서 해당 오류를 출력하는 것일까?

바로 functionB 안에 있는 'b' 때문이다. functionB 안에는 내부적으로 변수 'b'가 선언되지 않았다.

스크립트 스코프 변수인 a는 어떤 함수에서도 접근가능하지만, b나 c 같은 경우에는 각각 functionA 와 functionB 내부에서 선언된 변수라 외부에서 접근하지 못한다.

 

그럼, 이런 문제를 해결할 순 없는 것일까?

아니다! Closure 를 사용하면 해결할 수 있다. 아래 코드를 살펴보자.

let a = 'a';
function functionA() {
    function functionB() {
        let c = 'c';
        console.log(a, b, c);
    }
    let b = 'b';
    console.log(a, b);
    functionB();
}

functionA();

 

해당 코드는 아까 전 코드에서 functionB를 functionA 내부로 단순히 가져온 코드이다.

functionA가 호출되면, console.log(a,b) 가 먼저 출력되고, functionB가 호출된다.

functionB를 타고 들어가면 console.log(a,b,c) 를 실행하는데, 이 때 아래와 같이 정상적으로 출력결과가 나타난다. 

클로저를 사용

아까 전 코드에서는 functionA가 functionB의 외부함수였지만, functionB를 functionA 내부로 가져온다면, functionB 기준에서는 function A가 외부함수인데도, 변수 b를 잘 가져온 것을 확인할 수 있다.

스코프를 확인해본다면, 아래와 같이 변수 b가 closure 스코프가 된 것을 알 수 있다.

변수 b는 클로저 스코프

이처럼, 다른 함수 내부에 정의된 함수(innerFunction)가 있는 경우, 외부 함수(outerFunction)가 실행을 완료하고 해당 변수가 해당 함수 외부에서 더 이상 엑세스할 수 없는 경우에도 해당 내부함수는 외부함수의 변수 및 범위에 엑세스할 수 있다.


< 구조 분해 할당 (ES6) >

- 배열이나 객체의 속성을 해체하여 그 값을 개별 변수에 담을 수 있게 하는 JavaScript 표현식이다.

destructing

 

구조 분해 할당은 Clean Code 를 위해서 더 깔끔한 코드를 위해서 사용한다.

객체 구조 분해 할당의 예시를 한 번 살펴보자.

(ex1)

function buildAnimal(animalData){
    let animal = animalData.animal,
    let color = animalData.color,
    let hairType = animalData.hairType,
    let accessory = animalData.accessory;
    ...
}

let obj = {
    accessory : 'horn',
    animal : 'horse',
    color : 'purple',
    hariType : 'curly'
}

 

객체 부분을 보면, buildAnimal 이라는 함수에 인자를 줄 때, animalData를 넣어준다. 이 animalData에 들어있는 게 animal, color, hairType, accessory 들을 하나씩 가져와 주려면 하나하나씩 다 가져와 줬다. 너무 길게 작성될 염려가 있다는 단점이 있다.

 

그래서 이걸 좀 깔끔한 클린코드로 만들려면 객체 구조 분할을 이용해서 아래와 같이 표현할 수 있다.

function buildAnimal(animalData){
    let {accessory, animal, color, hairType} = animalData;
    ...
}

 

그렇다면, 더 깊게 들어간 객체 구조 분해 할당을 살펴봐보자.

(ex2)

let pet = {
    name : "Allu",
    age : 10,
    birth : '0602',
    information : {
        species : 'dachshund',
        color : 'black'
        gender : 1
    }
}

 

위 코드에서는 pet 안에 information 가 들어 있다. 객체의 속성을 해체하여 species, color, gender 등의 개별 변수를 담아내려면 더 깊게 들어간 객체 구조분해 할당을 할 수 있다.

let {information : {species,color,gender}} = pet;

console.log(species, color, gender);   // 'dachshund', 'black', 1

 

이제는 배열 구조분해 할당을 살펴보자.

아래와 같은 코드가 있다고 해보자.

(ex1)

let a, b, rest;
[a,b] = [10,20];

console.log(a); // 10
console.log(b); // 20

[a,b,...rest] = [10,20,30,40,50];

console.log(rest);  // [30,40,50]

 

a, b 는 각가 10과 20 이 되는 것은 직관적으로 알 수 있다. 그럼, rest 부분은 어떻게 나올까?

rest 를 spread operator 와 같이 이용하면, rest에는 배열의 나머지가 할당되는 것을 할 수 있다.

즉, a, b를 제외한 [30,40.50] 이 된다.

 

(ex2)

const week = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday'];
const day1 = week[0];
const day2 = week[1];
const day3 = week[2];
const day4 = week[3];
const day5 = week[4];

 

위 코드도  아래와 같이 배열 구조분해 할당을 할 수 있다.

const week = ['monday','tuesday','wednesday','thursday','friday'];

const [day1,day2,day3,day4,day5] = week;

 

생략도 가능하다. (,,)를 활용하면 된다.

(ex3)

const numbers = [1,2,3,4,5,6];

const [,,three,,five] = numbers;

 

다른 이름 변수명을 사용할 수도 있다.

(ex4)

const studentDetails = {
    firstName : 'Allu',
    lastName : 'Kim',
}

const {firstName: fName = 'not given', lastName} = studentDetails;

console.log(fName);
console.log(lastName);

 

firsName 말고 fName 이라는 변수로 대체해서 사용할 수 있다. 그리고 위 코드에서는 'Allu' 라는 firstName이 있는데 반해, 없다면 'not given' 이라는 fName을 출력할 것이다. ( 위 코드에서는 fName을 출력할 때, Allu 를 출력하겠다.)

 

마지막으로 사용사례를 한 번 살펴보자.

(ex5)

var people = [
    {
        name : "Mike Smith",
        family : {
            month : "Jane Smith",
            father : "Harry Smith",
            sister : "Samantha Smith",
        },
        age : 35    
    },
    {
        name : "Tom Jones",
        family : {
            mother : "Norah Jones",
            father : "Richard Jones",
            brother : "Howard Jones",
        },
        age : 25
    }
];

for (var {name: n, family : {father: f } } of people) {
    console.log("Name:  " + n + ", Father: " + f);
}

// "Name: Mike Smith, Father: Harry Smith"
// "Name: Tom Jones, Father: Richard Jones"

 

위 코드에서 구조 분해할당을 할때, father는 f 로 정의를 하고 name은 n으로 정의를 했다.


< 전개 연산자(Spread Operator) >

- 전개 연산자는 ECMAScript6(ES6) 에서 새롭게 추가됐으며, 특정 객체 또는 배열의 값을 다른 객체, 배열로 복제하거나 옮길 때 사용한다. 연산자의 모양은 ... 과 같이 생겼다.

 

1) 배열 조합

const arr1 = [1,2,3];
const arr2 = [4,5,6];
const arr3 = [7,8,9];
const arrWrap = arr1.concat(arr2,arr3);

console.log(arrWrap); // [1,2,3,4,5,6,7,8,9]

 

위 코드를 전개 연산자(Spread Operator)를 사용해서 아래와 같이 표현할 수 있다.

const arr1 = [1,2,3];
const arr2 = [4,5,6];
const arr3 = [7,8,9];
const arrWrap = [...arr1, ...arr2, ...arr3];

console.log(arrWrap); // [1,2,3,4,5,6,7,8,9]

 

비슷한 예제로

const arr1 = [1,2,3];
const arr2 = [4,5];
Array.prototype.push.apply(arr1,arr2);

console.log(arr1); // [1,2,3,4,5]

 

그냥 push 에 배열을 전달하면 배열 내부에 배열이 들어간다.

그래서 concat을 사용해도 되지만, concat은 기존 배열을 사용하지 않고 새 배열에 만들어서 반환한다. 

기존 배열에 배열을 추가하고 싶을 때는 push.apply() 를 사용해야 한다.

const arr1 = [1,2,3];
const arr2 = [4,5];
arr1.push(...arr2);

console.log(arr1); // [1,2,3,4,5]

 

Array.prototype을 사용하지 않고, spread operator를 사용해서 arr1.push(...arr2)로 바로 표현가능하다.

 

2) 객체 조합

const obj1 = {
    a : 'A',
    b : 'B'
};

const obj2 = {
    c : 'C',
    d : 'D'
};

const objWrap = {obj1, obj2};
console.log(objWrap);

 

objWrap에 두 객체 obj1, obj2 를 각각 넣으면 객체 자체가 들어간다. 때문에 objWrap은 아래와 같은 결과를 나타낸다.

{
    obj1 : {
        a : 'A',
        b : 'B'
    },
    obj2 : {
        c : 'C',
        d : 'D'
     }
}

 

반면, spread operator 를 사용하면,  객체 자체가 아닌 각각의 값이 할당된다.

const obj1 = {
    a : 'A',
    b : 'B'
};

const obj2 = {
    c : 'C',
    d : 'D',
}
const objWrap = {...obj1,...obj2};
console.log(objWrap);
{
    a : 'A',
    b : 'B',
    c : 'C',
    d : 'D'
}

 

objWrap 에 각 객체에 있는 값들이 할당된 것을 확인할 수 있다.

 

3) 기존 배열을 보존하는 spread operator

const arr1 = [1,2,3];
const arr2 = arr1.reverse();

console.log(arr1); // [3,2,1]
console.log(arr2); // [3,2,1]

 

단순히 배열에 reverse() 메서드를 사용해서 뒤집으면 위와 같이 기존 배열인 arr1 도 배열이 뒤집혀진 것을 확인할 수 있다.

 

원본 배열 유지를 하기 위해서는 spread operator를 사용하면 되는데,

const arr1 = [1,2,3];
const arr2 = [...arr1].reverse();

console.log(arr1); // [1,2,3]
console.log(arr2); // [3,2,1]

 

위 코드처럼 단순히 arr1 가 아니라 [...arr1] 과 같이 spread operator를 사용해서 만든 배열에 메서드를 적용하면 기존 배열은 유지된다.

불변성 유지를 할 수 있게 되는 것이다.


< Map, Filter, Reduce >

많은 메소드들이 있지만, 대표적으로 가장 많이 사용하는 메소드들인 map, filter, reduce 에 대해 살펴보자.

 

1) Map

- map() 메서드는 배열 내의 모든 요소 각각에 대하여 주어진 함수를 호출한 결과를 모아 새로운 배열을 반환한다.

const array1 = [1,4,9,16];

const map1 = array1.map(x => x*2);

console.log(map1);  // [2,8,18,32]

 

map 메소드는 아래와 같은 매개변수를 가진다. filter와 forEach와 같은 구문이라고 보면 된다.

array.map(callbackFunction(currenValue, index, array), thisArg)

 

callbackFunction, thisArg 두개의 매개변수가 있고
callbackFunction은 currentValue, index, array 3개의 매개변수를 갖는다.

  • currentValue : 배열 내 현재 값
  • index : 배열 내 현재 값의 인덱스
  • array : 현재 배열
  • thisArg : callbackFunction 내에서 this로 사용될 값

원래는, window 객체를 가리키지만, 다른 객체를 가리키게 하기 위해서는 thisArg에 원하는 객체를 넣어주면 된다.

const array1 = [1,4,9,16];

const map1 = array1.map(function (item, index, array) {
    console.log(item, index, array, this)
    return (item * 2)
}, {a: 'a'});

console.log(map1);

 

map 결과

 

2) Filter

- filter() 메서드는 주어진 함수의 테스트를 통과하는 모든 요소를 모아 새로운 배열로 반환한다.

const words = ['spray', 'limit', 'elite', 'exuberant', 'destruction', 'present'];
const result = words.filter(word => word.length > 6);

console.log(result); 
// ['exuberant', 'destruction', 'present']

 

filter() 메소드는 아래와 같은 매개변수를 가진다. 역시 map과 forEach와 같은 구문이라고 보면 된다.

array.filter(callbackFunction(element, index, array), thisArg)

 

filter() 메소드는 아래와 같은 매개변수를 가진다. 역시 map과 forEach와 같은 구문이라고 보면 된다.

const words = ['spray', 'limit', 'elite', 'exuberant', 'destruction', 'present'];

const result = words.filter(function (word, index, array) {
    console.log(word, index, array, this);
    return word.length > 6
}, { a: 'a'});

console.log(result);

 

3) Reduce

- reduce() 메서드는 배열의 각 요소에 대해 주어진 리듀서 (reducer) 함수를 실행하고, 하나의 결괏값을 반환한다.

구문을 살펴보면 아래와 같다.

array.reduce(reducerFunction, initialValue)

 

리듀서 함수는 4개의 인자를 가진다.

  • accumulator : 누산기 accmulator는 콜백의 반환값을 누적한다. 콜백의 이전 반환값 또는, 콜백의 첫 번째 호출이면서 initialValue를 제공한 경우에는 initialValue의 값이다.
  • currentValue : 처리할 현재 요소
  • index : 현재 인덱스, initialValue를 제공한 경우 0, 아니면 1부터 시작(필수요소는 아님)
  • array : reduce()를 호출한 배열

3-1) 두 번째 인수로 initialValue를 제공하지 않은 경우

//Reducer
[0,1,2,3,4].reduce(function (accumulator, currentValue, currentIndex, array) {
    return accumulator + currentValue;
});

두 번째 인수로 initialValue를 제공하지 않은 경우

 

3-2) 두 번째 인수로 initialValue를 제공한 경우

// InitialValue 제공

[0,1,2,3,4].reduce(function (accumulator, currentValue, currentIndex, array) {
    return accumulator + currentValue;
}, 10);

두 번째 인수로 initialValue를 제공한 경우


참고 자료

따라하며 배우는 자바스크립트 A-Z

반응형