LHJ

I'm a FE developer.

13.3 함수로서의 함수

19 May 2020 » js_lj

13.3 함수로서의 함수

function isCurrentYearLeapYear() {
    const year = new Date().getFullYear();
    if (year % 4 !== 0) return false;
    else if(year % 100 != 0) return true;
    else if (year % 400 != 0) return false;
    else return true;
}

함수의 분명한 특징에 대해 생각해 봤으니, 이제 함수를 함수로서 생각해 볼 시간입니다.
수학을 좋아한다면 함수를 일종의 관계로 생각할 수 있을 겁니다.
입력이 들어가면 결과가 나오는 관계 말입니다.
입력은 모두 어떤 결과와 관계되어 있습니다.
프로그래머들은 이렇게 함수의 수학적인 정의에 충실한 함수를 순수한 함수(pure function)라고 부릅니다.
하스켈(Haskell) 같은 언어는 오직 순수한 함수만 허용하기도 합니다.

그러면 순수한 함수는 우리가 여태까지 생각해 본 함수와 어떤 면에서 다를까요?
가장 중요한 차이는, 순수한 함수에서는 입력이 같으면 결과도 반드시 같다는 점입니다.
isCurrentYearLeapYear는 언제 호출하느냐에 따라서 true를 반환하기도 하고 false를 반환하기도 하므로 순수한 함수라고 할 수 없습니다.

둘째, 순수한 함수에는 부수 효과(side effect)가 없어야 합니다.
즉, 함수를 호출한다고 해서 프로그램의 상태가 바뀌어서는 안 된다는 뜻입니다.
지금까지 살펴본 함수에는 부수 효과가 없었습니다.
콘솔에 기록하는 것은 결과라고 봐야하니까요.
간단한 예를 하나 봅시다.

const colors = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet'];
let colorIndex = -1;
function getNextRainbowColor() {
    if (++colorIndex >= colors.length) colorIndex = 0;
    return colors[colorIndex];
}

getNextRainbowColor 함수는 호출할 때마다 무지개의 일곱 가지 색깔을 하나씩 반환합니다.
이 함수는 순수한 함수의 두 가지 정의를 모두 어깁니다.
입력이 같아도(매개변수가 없다는 점이 같습니다) 결과가 항상 다르고, 변수 colorIndex를 바꾸는 부수 효과가 있습니다.
colorIndex 변수는 getNextRainbowColor 함수에 속하지 않는데도 함수를 호출하면 변수가 바뀝니다.
이것은 부수 효과입니다.

잠시 윤년 문제로 돌아가서, 이 함수를 순수한 함수로 고치려면 어떻게 해야 할까요?
쉽습니다.

function isCurrentYearLeapYear(year) {
    if (year % 4 !== 0) return false;
    else if(year % 100 != 0) return true;
    else if (year % 400 != 0) return false;
    else return true;
}

새로운 함수는 입력이 같으면 결과도 항상 같고, 다른 효과는 전혀 없으므로 순수한 함수라고 볼 수 있습니다.

getNextRainbowColor 함수를 순수한 함수로 고치는 건 손이 조금 더 갑니다.
먼저 외부 변수를 클로저로 감싸는 방법을 봅시다.

const getNextRainbowColor = (function() {
    const colors = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet'];
    let colorIndex = -1;
    return function() {
        if (++colorIndex >= colors.length) colorIndex = 0;
        return colors[colorIndex];
    }
})();

이제 부수 효과가 없어졌지만, 아직은 입력이 같아도 결과가 다를 수 있으므로 순수한 함수라고 볼 수는 없습니다.
이 문제를 해결하려면 이 함수를 어떻게 사용할 것인지 주의 깊게 생각해야 합니다.
아마 이 함수는 반복적으로 호출할 겁니다.
예를 들어, 브라우저에서 어떤 요소의 색깔을 0.5초마다 바꾸고 싶다면 다음과 같은 코드를 쓰게 될 겁니다(브라우저 코드에 대해서는 18장에서 더 배웁니다).

const getNextRainbowColor = (function() {
    const colors = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet'];
    let colorIndex = -1;
    return function() {
        if (++colorIndex >= colors.length) colorIndex = 0;
        return colors[colorIndex];
    }
})();

setInterval(function() {
    document.querySelector('.rainbow')
        .style['background-color'] = getNextRainbowColor();
}, 500)

테스트파일

// 테스트파일 소스
const getNextRainbowColor = (function() {
    const colors = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet'];
    let colorIndex = -1;
    return function() {
        if (++colorIndex >= colors.length) colorIndex = 0;
        return colors[colorIndex];
    }
})();
setInterval(function() {
    document.getElementsByTagName('body')[0]
        .style['background-color'] = getNextRainbowColor();
}, 500)

이 코드에 별 문제가 없어 보이고, 의도도 분명히 드러납니다.
클래스가 rainbow인 HTML 요소의 색깔을 계속 바꾸는 거죠.
문제는, 만약 프로그램의 다른 부분에서 getNextRainbowColor()를 호출한다면 이 코드도 그 영향을 받는다는 겁니다.

이제 부수 효과가 있는, 다시 말해 외부에 영향을 주는 함수가 과연 좋은 것인지 생각해 볼 만한 시기입니다.
여기서는 이터레이터를 사용하는 것이 더 나은 방법입니다.

function getRainbowIterator() {
    const colors = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet'];
    let colorIndex = -1;
    return {
        next() {
            if (++colorIndex >= colors.length) colorIndex = 0;
            return { value: colors[colorIndex], done: false };
        }   
    }
}

이제 getRainbowIterator는 순수한 함수입니다.
이 함수는 항상 같은 것(이터레이터)을 반환하며 외부에 아무 영향도 주지 않습니다.
사용법이 바뀌긴 했지만, 훨씬 안전합니다.

function getRainbowIterator() {
    const colors = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet'];
    let colorIndex = -1;
    return {
        next() {
            if (++colorIndex >= colors.length) colorIndex = 0;
            return { value: colors[colorIndex], done: false };
        }   
    }
}

const rainbowIterator = getRainbowIterator();
setInterval(function() {
    document.querySelector('.rainbow')
        .style['background-color'] = rainbowIterator.next().value;
}, 500)

결국 next() 메서드는 매번 다른 값을 반환할 테니, 문제를 뒤로 미뤘을 뿐 아니냐고 생각할 수도 있습니다.
틀린 말은 아니지만, next()는 함수가 아니라 메서드라는 점에 주목할 필요가 있습니다.
메서드는 자신이 속한 객체라는 컨텍스트 안에서 동작하므로 메서드의 동작은 그 객체에 의해 좌우됩니다.
프로그래므이 다른 부분에서 getRainbowIterator를 호출하더라도 독립적인 이터레이터가 생성되므로 다른 이터레이터를 간섭하지 않습니다.