순수 함수란?
MDN 문서에 따르면, 함수란 자신의 외부(재귀 함수의 경우 스스로) 코드가 호출할 수 있는 '하위 프로그램'이다. 명령문의 시퀀스로 구성된 함수 본문을 가지고, 함수에 값을 전달하면, 함수는 값을 반환한다.
설명이 어렵다. 나름대로 정의를 풀어보면, 함수는 입력(매개 변수)을 받아, 함수 본문의 로직을 따라 처리하고, 결과를 반환하는 프로그램이다. 정확한 정의인지 모르겠지만, 아무튼 입력-처리-결과 이 세 가지가 함수를 이루는 중요한 요소라는 건 분명하다.
자바스크립트의 함수는 다른 함수의 매개 변수로 전달될 수 있고, 반환 값이 될 수도 있으며, 변수에 할당도 가능하다(아래 예시 코드 모두 다 문제없이 실행 가능하다).
// 변수에 할당
const myFunc1 = console.log; // console 객체에 있는 log 메서드를 myFunc1에 할당
function myFunc2(func) {
// 함수를 반환
return function () {
func('hi');
}
}
// 매개 변수로 함수를 사용
myFunc2(myFunc1)(); // hi
자바스크립트의 함수가 1급 객체라는 점 덕분에 함수에 함수를 매개 변수로 전달하거나 반환하는 '고차 함수'의 사용이 가능하다. 이는 리액트의 함수형 컴포넌트를 활용한 '고차 컴포넌트'와도 연결되는 개념이 되며, 이에 대해서는 나중에 조금 더 공부해 본 후 포스팅을 남겨볼 예정이다.
오늘 좀 더 깊게 공부해본 내용은 '순수 함수'다. 순수 함수가 자바스크립트에만 있는 개념은 아니며, 함수가 1급 객체가 아닌 프로그래밍 언어에서도 순수 함수는 존재한다.
프로그래밍에서 얘기하는 '순수 함수'에는 두 가지 조건이 붙는다.
- 입력이 같으면 언제나 출력도 같다.
- 외부에 어떠한 영향도 끼치지 않는다.
입력이 같으면 언제나 출력도 같다.
수학에서의 함수를 생각해보면, 보통 x
와 y
와 같은 미지수를 n개 가지며, y = 2x
처럼 어떠한 상황을 조합한 함수식이 만들어진다.
y = 2x
에서 x
와 y
에는 어떠한 값도 올 수 있다. 하지만, 어떠한 값이더라도 x
와 y
에 값이 정해지면 결과도 함께 정해진다. x
가 1이면 y
는 2가 되고, x
가 2면 y
는 4가 된다. y = 2x
라는 함수에서 x
에 2를 대입했는데 y
가 5가 되는 경우는 절대로 없다(불가능하다).
해당 공식을 코드로 작성해봤다. y = 2x
가 입력(x
)이 같으면 항상 같은 값을 반환하는 것처럼, double
함수도 그렇다. 이렇게, 어떠한 경우더라도 같은 입력에 같은 결과를 반환하는 함수는 순수 함수다.
function double(x) {
return 2 * x
}
아래는 입력이 같아도 같은 출력이 보장되지 않는 형태로 '안 순수한' 함수의 대표적인 예시이다(불순 함수, 또는 비순수 함수 등의 용어로 사용되는 것 같다).
let num = 0;
function sum(x) {
return num + x
}
얼핏 보면 x
에 1을 넣으면 1, 2를 넣으면 2가 나오니까 '순수한 거 아닌가?'라고 생각할 수 있다. 하지만, 함수 외부에 있는 num
이라는 변수는 sum
함수 이외의 어디에서도 바뀔 수 있는 값이다. 따라서, sum(1)
이라는 실행이 언제나 결과 1을 보장하진 않는다. 언제는 num
이 0이었어서 결과가 1일 수 있지만, 코드를 실행하다 다른 곳에서 num
을 1로 바꿔줬다면 sum(1)
을 다시 실행했을 때 결과가 1이 아니라 2가 된다.
순수 함수는 프로그램이 실행되는 모든 상황을 고려하더라도 언제나 같은 입력에 같은 출력이 나와야 한다. 위의 함수를 순수 함수가 되도록 고쳐보면 다음과 같다.
/** num이 매개변수가 되면 '같은 입력에 같은 출력'이라는 규칙에 부합하도록 동작시킬 수 있다 **/
let num = 0;
function sum(num, x) {
return num + x
}
/** 또는, 함수 외부의 변수를 내부로 옮겨서 외부 동작에 의한 변화를 막아줄수도 있다. **/
function sum(x) {
let num = 0;
return num + x
}
이러한 순수 함수의 특성은 언제, 어디서, 어떻게 함수가 실행되더라도 항상 동일한 결과가 나온다는 것을 보장해준다. 이는 '안정적인 프로그래밍'에 있어서 너무나도 중요한 부분이다. 어떠한 코드를 실행했을 때 예상한 동작이 그대로만 출력될 수 있다면 여러 예상치 못한 에러나 문제 상황들을 피할 수 있을 것이다.
외부에 어떠한 영향도 끼치지 않는다.
다음으로, 순수 함수는 함수 외부에 어떠한 영향도 끼치지 않는다는 중요한 특징이 있다. 여기서 '영향'은 다양한 의미를 가지는데, 그중 대표적으론 함수 외부에 있는 값을 직접 바꾸는 것이다.
아래 함수는 전역 변수의 값을 함수 내부에서 바꿔주고 있다. 함수가 실행되고 나면 함수 외부에 있던 변수의 값이 달라지기 때문에, 해당 함수는 비순수 함수다.
let num = 0;
function increase() {
return num++
}
아래 경우는 어떨지 살펴보자.
let num = 0;
function increase() {
return num++
}
function sumAfterIncrease(x) {
return x + increase();
}
함수 increase
는 외부 변수의 값을 변경시킬 가능성을 갖기 때문에 비순수 함수다. 그리고 sumAfterIncrease
는 increase
함수를 함수 내부에서 사용하고 있는데, increase
가 비순수 함수이기 때문에 sumAfterIncrease
는 언제나 같은 입력에 같은 출력을 보장받는다는 규칙을 지키지 못하게 된다. 따라서, 비순수 함수가 함수 본문 내용에서 호출되는 경우엔 마찬가지로 비순수 함수가 된다.
이런 상황에서, 함수가 외부에 끼치는 영향을 사이드 이펙트(부수 효과) 라고 부른다. 위 예시에선 외부에 있는 변수의 값을 바꾸는 사이드 이펙트가 발생한 것이다.
예시에서는 사이드 이펙트의 아주 대표적인 사례만 간단하게 살펴봤지만, 실제로 개발을 할 때 발생하는 사이드 이펙트의 종류는 다양하다. 외부 API에 요청을 보내는 것도 서버의 상태를 바꾸기 때문에 사이드 이펙트이고, console.log
나 console.error
처럼 브라우저 콘솔에 값을 출력하는 명령, alert
처럼 브라우저에 경고 창을 띄우는 상황, History API를 사용하여 페이지 사용 기록을 저장하는 것 등 '외부 환경'이라고 할 수 있는 것에 영향을 주는 모든 것은 사이드 이펙트이다. 그리고, 이러한 사이드 이펙트를 발생시키는 함수는 비순수 함수가 된다.
그래서, 어떻게 해야 하는가?
순수 함수는 안정적인 프로그램의 기초가 되기 때문에 최대한 지향해야 한다. 하지만, 프로그램을 개발하면서 모든 사이드 이펙트를 통제하는 것은 불가능하며, 언젠가-어디에선가는 무엇인가를 바꾸거나 영향을 주는 게 필요하다. 그렇게 값을 바꿔주고, 어떠한 '영향'을 발생시키는 것이 프로그래밍의 본질일지도 모른다.
다만, '의도하지 않은 사이드 이펙트'가 문제가 된다. 따라서, 최대한 많은 함수를 순수하게 유지하고, '사이드 이펙트 처리'라는 의도를 담은 함수를 별도로 관리하는 게 좋다. 그래야 프로그램에 문제가 생겼을 때 문제가 될만한 비순수 함수를 찾아가 해결하기에 유리하다.
함수를 순수하게 유지하기 위해 이런저런 자료들에서 팁을 주고 있었는데, 그중 몇 가지만 기록해 둔다.
순수 함수인지 판별하는 법
사이드 이펙트의 다양한 사례가 있다 보니, '이게 순수 함수가 맞나?'를 판별하기가 어렵다는 생각이 들었다. 그러다 어떤 자료에서 만약 함수가 반환값을 사용하지 않더라도 사용하는 것의 의미가 있는 경우라면 해당 함수는 비순수 함수일 가능성이 높다 라는 문구를 본 기억이 난다.
위에 작성했던 예시들도 자세히 살펴보면, 굳이 return
을 하지 않더라도 외부 변수의 값을 변경해 준다는 의미를 갖기 때문에, 명확히 비순수 함수에 해당한다는 걸 알 수 있다.
fetch
로 API 요청 보내기, console.log
로 콘솔 찍기, 이벤트 핸들러 함수에서 setState
실행 등은 전부 다 함수 내부에 있을 때 별도 return
이 없이도 의미를 갖게 만드는 행동의 예시다. 즉, 이런 명령들이 실행되는 함수는 비순수 함수라는 것을 인지하고, 의도적으로 이런 동작들은 별도의 함수에 떼놓는 게 바람직하다.
KISS 설계 원칙
KISS는 컴퓨터 과학에서 얘기되는 중요한 소프트웨어 설계 원칙 중 하나이다. Keep It Simple, Stupid의 줄임말이다. 어떠한 소프트웨어(함수)를 만들 때 Stupid가 와도 이해할 수 있을 정도로 단순하게 설계하라는 것이다.
함수의 사이즈가 커지고, 하나의 함수가 다양한 역할을 담당하게 될수록 함수는 순수하지 않을 가능성이 높다. 만약에 하나의 함수가 여러 역할을 담당하고 있다면 최대한 작게 쪼개보자. 그 과정에서 순수 함수와 비순수 함수를 명확하게 구분할 수도 있을 것이다.
여러 의미로, 함수의 사이즈가 커지는 것을 경계하고, 작게 만들려고 노력해야 한다.
결론
점점 더 사이즈가 큰 프로그램을 만들어 볼수록 의도치 않은 동작을 하는 코드를 자꾸 생산하게 된다. 코드를 쓸 땐 최대한 작은 단위로 목적을 분명히 해서 설계하고, 그렇게 설계한 탄탄한 부품을 차근차근 쌓아가면서 결과물을 만들어가야 한다. 그런 의미에서, 가장 기본 부품이 되는 함수를 순수하게 유지하는 것은 중요하다. 앞으로 코드를 짜면서 계속 상기시키고, 좋은 구조로 함수를 만들 수 있게 노력해야겠다.
'JavaScript' 카테고리의 다른 글
this 바인딩을 예측하기 어려운 상황 예시 (2) | 2024.11.19 |
---|---|
Date 객체 사용하기 (2) | 2024.11.15 |
프로토타입 톺아보기 (1) | 2024.11.13 |
241102 TIL (1) | 2024.11.03 |
자바스크립트 엔진과 런타임 (2) | 2024.10.30 |