본문 바로가기
Study/Javascript 왕초보

[Javascript] 12장 함수(2) (deep dive)

by jeongwle 2022. 5. 17.
728x90
반응형

참조에 의한 전달과 외부 상태의 변경

// 매개변수 primitive는 원시 값을 전달받고, obj는 객체를 전달받는다
function changeVal(primitive, obj){
    primitive += 100;
    obj.name = 'Kim';
}

// 외부 상태
var num = 50;
var person = { name: 'Lee' };

console.log(num); // 50
console.log(person); // {name: "Lee"}

changeVal(num, person);

console.log(num); // 50
console.log(person); // {name: "Kim"}

원시 타입 인수를 받은 매개변수 primitive의 경우 원시 값은 변경 불가능한 값이므로 직접 변경할 수 없기 때문에 재할당을 통해 할당된 원시 값을 새로운 원시 값으로 교체했고, 객체 타입 인수를 전달받은 매개변수 obj의 경우, 객체는 변경 가능한 값이므로 직접 변경할 수 있기 때문에 재할당 없이 직접 할당된 객체를 변경했다.

 

이처럼 함수가 외부 상태를 변경하면 상태 변화를 추적하기 어려워진다. 이는 코드의 복잡성을 증가시키고 가독성을 해치는 원인이 된다. 이러한 현상은 객체가 변경할 수 있는 값이며, 참조에 의한 전달 방식으로 동작하기 때문에 발생하는 부작용이다. 객체의 변경을 추적하려면 옵저버 패턴 등을 통해 객체를 참조를 공유하는 모든 이들에게 변경 사실을 통지하고 이에 대처하는 추가 대응이 필요하다. 이러한 문제의 해결 방법 중 하나는 객체를 불변 객체로 만들어 사용하는 것이다. 객체를 새롭게 생성하는 비용은 들지만 원시 값처럼 변경 불가능한 값으로 동작하게 만드는 것이다. 객체의 상태 변경이 필요한 경우에는 객체의 방어적 복사를 통해 원본 객체를 깊은 복사를 통해 새로운 객체를 생성하고 재할당을 통해 교체한다.

 

외부 상태를 변경하지 않고 외부 상태에 의존하지도 않는 함수를 순수 함수라 한다. 순수 함수를 통해 부수효과를 최대한 억제하여 오류를 피하고 프로그램의 안정성을 높이려는 프로그래밍 패러다임을 함수형 프로그래밍이라 한다. 순수 함수와 비순수 함수는 12장에서 더 살펴보자.

 

다양한 함수의 형태

1. 즉시 실행 함수

함수 정의와 동시에 즉시 호출되는 함수. 단 한번만 호출되며 다시 호출할 수 없다. 즉시 실행 함수는 반드시 그룹 연산자 (...)로 감싸야 한다.

// 익명 즉시 실행 함수
(function () {
    var a = 3;
    var b = 5;
    return a * b;
}());

 

즉시 실행 함수는 함수 이름이 없는 익명 함수를 사용하는 것이 일반적이다. 함수 이름이 있는 기명 즉시 실행 함수도 사용할 수 있다. 하지만 다시 호출할 수는 없다.

// 기명 즉시 실행 함수

(function foo() {
    var a = 3;
    var b = 5;
    return a * b;
}());

foo(); // ReferenceError: foo is not defined

 

그룹 연산자로 함수를 묶는 이유는 먼저 함수 리터럴을 평가해서 함수 객체를 생성하기 위함이다. 그룹 연산자 이외의 연산자를 사용하여 즉시 실행 함수를 만드는 방법이 있지만 이 책에서는 그룹연산자를 사용한다.

// 즉시 실행 함수를 만드는 방법
(function () {
    //...
}());

(function () {
    //...
})();

!function () {
    //...
}();

+function () {
    //...
}();

즉시 실행 항숨도 일반 함수처럼 값을 반환할 수 있고 인수를 전달할 수도 있다.

 

2. 재귀함수

함수가 자기 자신을 호출하는 것을 재귀 호출이라 한다. 재귀 함수는 자기 자신을 호출하는 행위인 재귀 호출을 수행하는 함수를 말한다. 재귀 함수는 반복되는 처리를 위해 사용한다.

function countdown(n) {
    if (n < 0) return; // 탈출 조건
    console.log(n);
    countdown(n - 1); // 재귀 호출
}

countdown(10);

 

재귀 함수는 자신을 무한 재귀 호출하기 때문에 재귀 호출을 멈출 수 있는 탈출 조건을 반드시 만들어야 한다. 대부분의 재귀 함수는 for 문이나 while 문으로 구현 가능하다.

function countdown(n){
    while (n >= 0){
        console.log(n);
        n--;
    }
}

countdown(10);

 

재귀 함수는 반복되는 처리를 반복문 없이 구현할 수 있다는 장점이 있지만 무한 반복에 빠질 위험이 있고 이로 인해 스택 오버플로 에러를 발생시킬 수 있으므로 사용에 주의하여야 한다. 따라서 재귀 함수는 반복문을 사용하는 것 보다 재귀 함수를 사용하는 편이 더 직관적으로 이해하기 쉬울 때만 한정적으로 사용하는 것이 바람직하다.

 

3. 중첩함수

함수 내부에 정의된 함수를 중첩 함수 또는 내부 함수라 한다. 그리고 중첩 함수를 포함하는 함수를 외부 함수라 부른다. 중첩 함수는 외부 함수 내부에서만 호출할 수 있다. 일반적으로 중첩 함수는 자신을 포함하는 외부 함수를 돕는 헬퍼 함수의 역할을 한다.

function outer() { // 외부 함수
    var x = 1;
    
    // 중첩 함수 or 내부 함수
    function inner(){
        var y = 2;
        // 외부 함수의 변수를 참조할 수 있다.
        console.log(x + y); // 3
    }
    inner();
}

outer();

ES6부터 함수 정의는 문이 위치할 수 있는 문맥이라면 어디든지 가능하다. if 문이나 for 문 등의 코드 블록 내에서도 정의할 수 있다. 단, 호이스팅으로 인해 혼란이 발생할 수 있으므로 if 문이나 for 문 등의 코드 블록에서 함수 선언문을 통해 함수를 정의하는 것은 바람직하지 않다. 중첩 함수는 스코프와 클로저에 깊은 관련이 있다. 이에 대해서는 나중에 살펴보자.

 

4. 콜백함수

함수의 매개변수를 통해 다른 함수의 내부로 전달되는 함수를 콜백 함수 라고 하며, 매개 변수를 통해 함수의 외부에서 콜백 함수를 전달받은 함수를 고차 함수라고 한다. 매개변수를 통해 함수를 전달받거나 반환값으로 함수를 반환하는 함수를 함수형 프로그래밍 패러다임에서 고차 함수라 한다.

function repeat(n, f) {
    for (var i = 0; i < n; i++){
        f(i); // i를 전달하면서 f를 호출
    }
}

var logAll = function (i) {
    console.log(i);
}

repeat(5, logAll); // 0 1 2 3 4

var logOdds = function (i) {
    if (i % 2) console.log(i);
}

repeat(5, logOdds); // 1 3

 

콜백 함수가 고차 함수 내부에만 호출된다면 콜백 함수를 익명 함수 리터럴로 정의하면서 곧바로 고차 함수에 전달하는 것이 일반적이다.

// 익명 함수 리터럴을 콜백 함수로 고차 함수에 전달한다.
// 익명 함수 리터럴은 repeat 함수를 호출할 때마다 평가되어 함수 객체를 생성한다.

function repeat(n, f) {
    for (var i = 0; i < n; i++){
        f(i);
    }
}

repeat(5, function (i) {
    if (i % 2) console.log(i);
}); // 1 3

따라서 콜백 함수를 다른 곳에서도 호출할 필요가 있거나, 자주 호출된다면 함수 외부에서 콜백 함수를 정의한 후 함수 참조를 고차 함수에 전달하는 편이 효율적이다.

 

콜백 함수는 함수형 프로그래밍 패러다임뿐만 아니라 비동기 처리(이벤트 처리, Ajax 통신, 타이머 함수 등)에 활용되는 중요한 패턴이다.

// 콜백 함수를 사용한 이벤트 처리
// myButton 버튼을 클릭하면 콜백 함수를 실행한다.
document.getElementById('myButton').addEventListener('click', function (){
    console.log('button clicked!');
});

// 콜백 함수를 사용한 비동기 처리
// 1초 후에 메시지를 출력한다.
setTimeout(function () {
    console.log('1초 경과');
}, 1000);

 

콜백 함수는 비동기 처리뿐 아니라 배열 고차 함수에서도 사용된다. 자바스크립트에서 배열은 사용 빈도가 매우 높은 자료구조이고 배열을 다룰 때 배열 고차 함수는 매우 중요하다. 이에 대해서는 27장에서 더 자세하게 살펴보자.

// 콜백 함수를 사용하는 고차 함수 map
var res = [1, 2, 3].map(function (item) {
    return item * 2;
});

console.log(res); // [2, 4, 6]

// 콜백 함수를 사용하는 고차 함수 filter
res = [1, 2, 3].filter(function (item) {
    return item % 2;
});

console.log(res); // [1, 3]

// 콜백 함수를 사용하는 고차 함수 reduce
res = [1, 2, 3].reduce(function (acc, cur) {
    return acc + cur;
}, 0);

console.log(res); // 6

위의 예제는 아직 어떻게 사용해야하는지 이해가 완벽히 되지는 않았지만 일단 27장에서 살펴보기로 했으니 눈으로만 보고 넘어가겠다.

 

순수 함수와 비순수 함수

함수형 프로그래밍에서는 어떤 외부 상태에 의존하지도 않고 변경하지도 않는 함수를 순수 함수라 하고, 외부 상태에 의존하거나 외부 상태를 변경하는 함수를 비순수 함수라고 한다.

순수 함수는 동일한 인수가 전달되면 언제나 동일한 값을 반환한다. 매개변수를 통해 함수 내부로 전달된 인수에게만 의존해 값을 생성해 반환하는 것이다. 함수 내부 상태에만 의존한다 해도 그 내부 상태가 호출될 때마다 변화하는 값(ex: 현재 시간)이라면 순수 함수가 아니다.

순수 함수는 일반적으로 최소 하나 이상의 인수를 전달 받는다. 언제나 동일한 값을 반환하므로 인수가 없다면 상수와 다름이 없고 그다지 의미가 없다. 또한 순수 함수는 인수를 변경하지 않는 것이 기본이다.

var count = 0;

// 순수 함수 increase는 동일한 인수가 전달되면 언제나 동일한 값을 반환한다.
function increase(n) {
    return ++n;
}

// 순수 함수가 반환한 결과값을 변수에 재할당해서 상태를 변경
count = increase(count);
console.log(count); // 1

count = increase(count);
console.log(count); // 2

 

반대로 비순수 함수는 외부 상태에 따라 반환값이 달라지고, 외부 상태를 변경하는 부수 효과가 있다. 

var count = 0;

// 비순수 함수
function increase() {
	return ++count; // 외부 상태에 의존하여 외부 상태를 변경한다.
}

// 비순수 함수는 외부 상태(count)를 변경하므로 상태 변화를 추적하기 어려워진다.
increase();
console.log(count); // 1

increase();
console.log(count); // 2

위 예제와 같이 인수를 전달받지 않고 함수 내부에서 외부 상태를 직접 참조하면 외부 상태에 의존하게 되어 반환값이 변할 수 있고, 외부 상태도 변경할 수 있으므로 비순수 함수가 된다. 함수 내부에서 외부 상태를 직접 참조하지 않더라도 매개변수를 통해 객체를 전달받으면 비순수 함수가 된다.

함수가 외부 상태를 변경하면 상태 변화를 추적하기 어려워진다.  따라서 함수 외부 상태의 변경을 지양하는 순수 함수를 사용하는 것이 좋다.

함수형 프로그래밍은 순수 함수와 보조 함수의 조합을 통해 외부 상태를 변경하는 것을 최소화하여 불변성을 지향하는 프로그래밍 패러다임이다. 로직 내에 존재하는 조건문과 반복문을 제거해서 복잡성을 해결하며, 변수 사용을 억제하거나 생명주기를 최소화해서 상태 변경을 피해 오류를 최소화하는 것을 목표로 한다. 함수형 프로그래밍은 순수 함수를 통해 부수 효과를 최대한 억제해 오류를 피하고 프로그램의 안정성을 높이려는 노력의 일환이라 할 수 있다.

728x90
반응형

댓글