순수 함수로 만드세요!


원문
Jack Franklin, https://alistapart.com/article/making-your-javascript-pure/

해당 글은 A List Apart와 Jack Franklin의 허락을 받아 번역하였습니다.
Translated with the permission of A List Apart and Jack Franklin.

웹사이트나 애플리케이션의 코드 줄 수가 증가하면 필연적으로 버그가 생긴다. 자바스크립트 뿐만 아니라 모든 언어가 버그 없이 무언가를 만드는것은 매우 어렵고, 불가능하다. 하지만 그것이 버그를 생기지 않도록 주의조차 할 수 없는 것을 의미하는 것은 아니다.

순수 함수와 비순수 함수

순수함수는 외부 스코프의 변수 값에 영향을 받지 않는 함수다. 설명이 조금 모호한데 실제 코드를 보며 설명하겠다.

마우스 이벤트를 받아 페이지를 세로로 2등분했을 때 왼쪽 영역인지를 감지하는 함수가 있다. 왼쪽이면 true를, 오른쪽이면 false를 반환한다. 실무 코드는 예제보다 조금 더 복잡하겠지만, 순수, 비순수 함수를 설명하기에 적합하다.

function mouseOnLeftSide(mouseX) {
  return mouseX < window.innerWidth / 2;
}

document.onmousemove = function(e) {
  console.log(mouseOnLeftSide(e.pageX);
};

mouseOnLeftSide는 x좌표를 받아 윈도우를 세로로 2등분 했을 때 좌측 영역에 포함되는지를 확인한다. 하지만, mouseOnLeftSide() 는 순수 함수가 아니다. 함수의 코드를 보면 내부에서 명시적으로 전달하지 않은 값을 참조하는 것을 알 수 있다.

return mouseX < window.innerWidth / 2;

함수를 실행할 때 mouseX는 전달된 값이지만 window.innerWidth는 그렇지 않다. 이 함수는 외부의 값을 참조하고 있다. 따라서 순수 함수가 아니다.

비순수 함수의 문제점

어떤 함수가 비순수 함수인 것이 왜 문제일까?

윈도우의 가로 너비가 500 픽셀 이하일 때 함수가 정확하지 않다는 버그를 리포팅 받았다고 하자. 어떻게 테스트 할 것인가? 두 가지 선택지가 있다.

  • 브라우저를 실행시키고 마우스를 이리저리 움직여가며 문제를 발생할때까지 테스트해 본다.
  • 단위 테스트를 작성해서 버그를 추적하는것 뿐만 아니라 같은 문제가 다시 발생하지 않도록 조치한다.

문제를 해결하고 재발하지 않게 하기 위해 두 번째 선택지를 따랐지만 새로운 문제를 직면했다. 테스트를 실행하기 위한 환경을 어떻게 만들어야 할까? 함수를 테스트하기 위해서는 윈도우의 가로 크기를 500픽셀 이하로 줄여야 한다. 하지만 어떻게 해야 할까? 코드 내부에서 window.innerWidth를 직접 참조하고 있는데 말이다.

순수 함수의 장점

1. 간단하게 테스트할 수 있다

아래와 같이 코드를 리펙토링했다.

function mouseOnLeftSide(mouseX, windowWidth) {
  return mouseX < windowWidth / 2;
}

document.onmousemove = function(e) {
  console.log(mouseOnLeftSide(e.pageX, window.innerWidth));
};

mouseOnLeftSide()의 주요 변경점은 바로 인자를 2 개 받는다는 것이다. 이제 이 함수는 순수함수가 되었다. 순수함수는 모든 변수를 명시적으로 받아야 하며 절대 그 외의 변수를 외부에서 참조하지 않아야 한다.

기능상으로 이전 예제와 같지만 유지보수하기 좋아졌고 테스트하기 역시 쉬워졌다. 이제 테스트를 위해 window.innerHTML를 조작하는 찜찜한 준비를 하는 대신 두 가지 인자를 넘기기만 하면 된다.

mouseOnLeftSide(5, 499)  // 500보다 작을때도 동작한다.

2. 코드 자체가 문서가 된다.

테스트가 쉬워질 뿐만 아니라, 순수함수는 코드 자체만으로 자신이 어떤 동작을 하는지 설명한다. 파라미터로 넘겨진 변수만 신경쓰면 된다. 반면에 비순수 함수는 코드에 외부의 변수를 참조하기 때문에 그 변수를 알지 못하는 이상 코드를 이해하기 어렵다. 다음의 예제를 보자.

function mouseOnLeftSide(mouseX, windowWidth)

코드 내부와 상관없이 이 함수는 두 파라미터를 받고 있고 파라미터 역시 명확한 이름을 사용하고 있다. 추후에 이 코드를 다시 보더라도 쉽게 이해할 수 있다.

3. 전역변수 사용을 피하게 된다

전역 변수를 참조하는 일은 아무리 문서화가 잘 되어있다고 해도 버그가 발생하는 주요 지점이 된다. 이는 전역 변수가 바뀌면 함수가 다르게 동작하기 때문이다.

순수함수의 또 다른 장점은 바로 참조 투명성 이다. 어려워 보이는 단어지만 뜻은 간단하다. 입력이 같으면 출력은 항상 같다는 뜻이다. mouseOnLeftSide를 보자.

function mouseOnLeftSide(mouseX) {
  return mouseX < window.innerWidth / 2;
}

이 함수는 참조 투명적이지 않다. 함수를 5번 호출하는 동안 윈도우 크기를 변경시키면 결과는 매번 달라진다. 예제 코드는 조금 계획적이라 문제를 발견하기 쉽지만 입력과 출력이 다른 코드는 다루기 매우 어렵다. 매번 동작이 같다는 것을 보장할 수 없기 때문이다. 같은 이유로 함수가 실행되는 조건을 설정하기 어렵기 때문에 테스트하기도 어렵다.

한편으로, 리펙토링한 mouseOnLeftSide는 외부 변수에 영향을 받지 않기 때문에 참조 투명적이다.

function mouseOnLeftSide(mouseX, windowWidth) {
  return mouseX < windowWidth / 2;
}

함수가 참조 투명적이 되었다. 입력으로만 연산을 하게 되었고 전체 클래스로부터의 사이드 이펙트나, 예외적인 동작을 하게 될 여지를 없앴다. 데이터를 주고 받는 과정을 완전히 제어할 수 있으므로 디버깅하기 역시 쉬워졌다.

어떤 함수를 순수하게 만들 것인가

순수 함수를 지속적으로 유지할수는 없다. 언젠간 바깥의 변수를 참조해야 하는 시기가 올 것이며 가장 일반적인 예는 DOM엘리먼트를 조작할 경우이다. 이럴 경우 아무렇게나 참조하지 말고 순수 함수와 비순수 함수를 계획적으로 나누는 것이 좋다.

아래의 코드를 보자. DOM엘리먼트를 찾아서 배경색을 붉게 만드는 함수다.

function changeElementToRed() {
  var foo = document.getElementById('foo');
  foo.style.backgroundColor = 'red';
}

changeElementToRed();

위 함수에는 두 가지 문제가 있다. 순수함수로 바꿔 해결할 수 있다.

  • 위 함수는 재사용하기 어렵다. 특정 DOM과 의존성이 있어 다른 엘리먼트에 적용할 수 없다.
  • 비순수함수라 테스트하기 어렵다. 테스트하려면 의존성이 있는 DOM엘리먼트를 만들어주어야 한다.

위의 두 개선점으로 리펙토링해 보았다.

function changeElementToRed(elem) {
  elem.style.backgroundColor = 'red';
}

function changeFooToRed() {
  var foo = document.getElementById('foo');
  changeElementToRed(foo);
}

changeFooToRed();

changeElementToRed()를 일반적인 DOM엘리먼트에도 사용할 수 있게 바꿨다. 앞서 언급했던 순수함수의 장점을 가졌다.

중요한 점은 아직 changeFooToRed()라는 비순수 함수가 있다는 점이다. 이 점은 피해갈 수 없다. 하지만 앞서 이야기했던대로 계획적으로 분리해 조금 더 발견하기 쉽게 만들었다. 이렇게 코드 전체에서 비순수함수의 비율을 줄여나가는 것이 중요하다.

결론

'순수함수', '사이드이펙트', '참조투명성' 이라는 단어는 함수형 언어와 관련이 있고 자바스크립트에도 충분히 적용할 수 있다. 이 원리를 잘 이해하고 적용하면 여러분의 코드는 좀 더 의미있고, 설명적이고, 쉽게 유지보수 할 수 있게 될 것이다. 이 방법을 자연스럽게 적용하는데는 조금 시간이 걸리겠지만 곧 그렇게 될 것이다. 훗날 후배 개발자들이 고마워할 수 있게 되지 않을까?

김민형2016.06.13
Back to list