페르난도 도글리오, 2020년 8월 11일
객체 지향 프로그래밍과 함수형 프로그래밍은 그 규칙들과 장단점이 뚜렷이 다른 두 개의 프로그래밍 패러다임들이다.
그러나 자바스크립트는 끝까지 쭉 한 가지 길을 따라가는 대신 두 가지 길의 딱 중간에 자리잡았다. 클래스, 객체, 상속과 같이 일반 객체 지향 언어가 가지고 있는 양상들 몇 가지를 제공하지만 그와 동시에 고차원 함수들 및 그것들을 합성할 수 있는 기능과 같은 몇몇 함수형 프로그래밍의 개념도 제공한다.
우리가 가장 좋아하는 이 언어가 완전히 함수형은 아니지만, 나는 그 양상들 중 몇 가지를 논하고 자바스크립트에서 그것들을 어떻게 이득이 되게 사용할지를 다루고 싶다.
고차원 함수
이 글에서 다루고 있는 세 가지 개념들 중 가장 중요한 것: 고차원 함수부터 시작하도록 하자.
고차원 함수에 접근할 수 있다는 뜻은 함수들이 코드에서 당신이 정의하고 불러낼 수 있는 생성자 이상이라는 것으로, 사실 이것들은 대입 가능한 독립체들이다.
이것은 당신이 자바스크립트를 좀 해봤다면 놀랍지 않을 것인 게, 어쨌든 당신은 온라인 예시에 따라 익명 함수를 상수에 간단히 대입할 수 있었을 것이다. 이런 것은 극도로 흔하다.
const adder = (a, b) => {
return a + b
}
놀라울 수도 있는 것은, 특히 자바스크립트가 당신이 프로그래밍을 배운 언어라면, 위의 논리는 다른 많은 언어들에서는 유효하지 않다는 것이다. 정수를 대입하듯이 함수를 대입할 수 있다는 것은 매우 유용하다. 사실 이 글에서 다룬 주제들 대부분이 그 부산물이다.
고차원 함수의 혜택: 행위 캡슐화
고차원 함수가 있기에 위와 같은 함수를 대입할 수 있을뿐만 아니라 함수를 부를 때 그것을 매개변수로 전달할 수도 있다. 이는 결국 함수를 하나의 인수로 직접 전달하여 복잡한 행동을 재사용할 수 있게 하는 매우 역동적인 코드베이스를 창작해 낼 문을 열어준다.
당신이 순수 객체 지향 환경에서 작업하고 있고, 당신이 이해하는 논리의 한 부분을 어떤 과업을 완수하기 위해 더 큰 코드의 일부분으로 사용되고 확장될수 있도록 하려 한다고 상상해보라. 이 경우 당신은 아마도 상속에 귀의할 것이다. 그 논리를 한 추상 클래스안에서 캡슐화 한 다음, 그것을 클래스들의 한 실행 세트에 추가하여 그 일반화된 논리를 이용하는 확장을 하는 것이다. 이것은 완벽한 객체 지향 프로그래밍 행위이며 작동도 되지만 우리가 방금 뭘, 왜 했는지 한 번 살펴보자. 우리는:
- 재사용 가능한 논리를 캡슐화 하기 위해 추상적 생성자를 만들었다.
- 부차적인 생성자를 만들었다
- 후자를 활용하기 위해서 전자에 확장하였다.
우리가 원했던 것은 논리를 재사용하는 것인데, 함수적으로 호환되는 환경에 접근할 수 있었다면 간단히 재사용 가능한 논리를 함수로 뽑아내어, 이 캡슐화된 행위를 사용함으로써 이득을 볼 수 있는 다른 어떤 함수에라도 매개 변수로 넘길 수 있었을 것이다. 그냥 논리를 재사용하기 위한 "보일러 판" 공정 같은 것은 없다. 우리는 그냥 함수들을 만들어내고 있는 것이다.
다음은 내가 위에서 설명한 것을 보여주기 위한 예시들이다. 첫번째 코드는 객체 지향 프로그래밍 환경에서 어떤 식으로 형식자를 재사용할 것인가를 보여준다.
//Encapsulated behavior
abstract class LogFormatter {
format(msg) {
return Date.now() + "::" + msg
}
}
//Reusing the behavior
class ConsoleLogger extends LogFormatter {
log(msg) {
console.log(this.format(msg))
}
}
class FileLogger extends LogFormatter {
log(msg) {
writeToFileSync(this.logFile, this.format(msg))
}
}
오해하지 말라. 두 가지 접근 다 장점이 있고 둘 다 매우 유효하며 여기서 잘못된 건 없고, 나는 그냥 여기서 이 접근이 믿기 힘들 정도로 얼마나 융통성 있는지, 그리고 마치 정수나 스트링과 같은 기본 타입처럼 행위를 (즉 함수를) 전달할 수 있다는 이유만으로 이게 어떻게 가능한지를 보여주고 있는 것이다.
고차원 함수의 혜택: 더 깔끔한 코드
이것의 좋은 예시는 물론 forEach, map, reduce등과 같은 배열 메소드이다. 예를 들어 C같은 비 함수 프로그래밍 호환 언어에서는 배열의 요소들을 반복처리하여 변형 적용을 하려면 for 룹 또는 여타 다른 룹 구조를 필요로 한다. 이것들을 쓸 때에는 명령식으로 코딩해야 하는 반면(다시 말해, 그 룹 안에서 어떻게 일이 일어나는지를 표현해 줄 필요가 있음), 함수적 접근을 사용할 수 있다는 것은 좀 더 서술적인 코딩 타입을 허용한다는 것이다. (무엇이 일어나야 하는지를 구체화하게 됨)
let myArray = [1,2,3,4]
let transformedArray = []
for(let i = 0; i < myArray.length; i++) {
transformedArray.push(myArray[i] * 2)
}
이 코드는 말 그대로 이렇게 얘기하는 것이다:
- myArray를 위한 인덱스로 사용되어 myArray 안에서 0에서부터 아이템의 갯수 만큼 값을 가지게될 새로운 변수 i를 선언한다.
- i의 모든 값마다 i의 위치에서 myArray의 값을 곱하고 그것을 transformedArray 배열에 넣는다.
이렇게 하면 되고, 상대적으로 이해하기는 쉽지만 논리의 복잡함이 쉽게 증가할 수 있으며, 이것을 독해하는 데 필요한 관련 인지력도 상승할 것이다. 그러나 아래와 같은 함수적 접근은 더 읽기 쉬울 것이다:
const double = x => x * 2;
let myArray = [1,2,3,4];
let transformedArray = myArray.map(double);
근본적으로, 이 코드는 말하고 있다:
- myArray의 요소들을 double 함수와 맵핑 하고 그 결과를 transformedArray에 대입하라.
읽기가 훨씬 쉽고 논리가 두 함수들(map과 double) 안에서 딴 데 숨어 있기 때문에 그것들이 어떻게 작동하는지 이해하는 것에 대해 걱정할 필요가 없다. 첫번째 예시에서도 함수 안에 곱셈 논리를 숨길 수 있지만 반복문은 거기 있어야 되고, 그 코드를 읽는 사람으로서는 그것이 어떻게 작동하는지 이해하기 위해 머릿 속에서 분석해야 한다는 것이 큰 부분이다.
다듬기
함수 다듬기는 다매개변수 함수를 매개변수를 적게 받고 남는 파라미터들을 고정시키는 함수로 변화시키는 행위이다. 예를 들어 설명하겠다.
//Take this function:
function adder(a, b) {
return a + b
}
//Turn it into:
const add10 = x => adder(a, 10)
그러면 이제, 만약 당신이 원했던 것은 그저 연속 값에 10을 더하는 것뿐이었다고 하면 adder를 부르는 대신 add10을 부를 수 있다. 이게 좀 웃기는 예시였다는 건 알지만 아마 당신이 다듬기를 찾아 볼 때마다 어디든 나올 것이다. 하지만 당신이 무엇을 하고 있는지를 고려하면, 당신은 adder 함수의 논리를 가져다가 그 함수의 특화된 버전을 만들고 있는 것으로, 다시 말해, 당신은 클래스에다 하는 것처럼 그 함수를 확장하고 있는 것이다.
다듬기를 함수형 프로그래밍에서의 상속으로 생각해도 되고, 그런 생각의 줄기를 따라가서 logger 예시로 돌아가면 이런 걸 얻을 수 있다:
//Your "abstract class"
function log(msg, msgPrefix, output) {
output(msgPrefix + msg)
}
function consoleOutput(msg) {
console.log(msg)
}
function fileOutput(msg) {
let filename = "mylogs.log"
writeFileSync(msg, filename)
}
//Actual "implementation classes"
const logger = msg => log(msg, ">>", consoleOutput);
const fileLogger = msg => log(msg, "::", fileOutput);
근본적으로 세 개의 매개 변수를 필요로 하는 log라 불리는 함수가 있고 우리는 그것을 한 가지 매개변수만 필요로 하는 특화된 버전으로 손질하고 있는 것이다. 왜냐면 다른 두 개는 이미 우리가 선택했기 때문에.
당신이 내 예시를 단순히 직접 갖다 쓰지는 않을 것이기 때문에, 내가 log 함수를 추상 클래스처럼 다루고 있다는 것을 기억하는 것이 중요하다. 그러나 이것은 그냥 정상적인 함수이기 때문에 이렇게 하는 데에는 한계가 없다. 클래스를 사용하고 있었다면 그것에서 직접 한 예시를 끌어낼 수 없었을 것이다.
합성
마지막으로, 합성함수는 고차원 함수를 즉시 이용할 수 있게 갖추고 있는 또 하나의 매우 흥미로운 부산물이다. 첫눈에 보면 합성을 다듬기의 사례로 쉽게 착각하거나 또는 그 반대로 생각할 수도 있는데, 즉각적인 값들(위의 logger 예시에서 했던 것처럼) 대신에 함수들을 다듬기 하는 것이 합성 함수라고 여겨질 수도 있다.
그리고 거의 맞다. 함수를 가지고 놀기 시작하다 보면 그 둘 사이에 매우 미세한 선이 존재한다. 특히 합성은 다음과 같이 정의된다:
컴퓨터 공학에서, 합성함수는 간단한 함수들을 합쳐 더 복잡한 것들을 만드는 방법이다. 수학에서 일반적인 함수들의 합성처럼, 각 함수의 결과가 그 다음 함수의 인수로 전달되고 마지막 함수의 결과가 전체의 결과가 된다.
이것은 위키피디아에서 발췌한 합성함수의 정의인데 마지막의 부분이 가장 중요한 부분이므로 내가 굵은글씨 처리 했다. 다듬기는 저런 제한이 없고, 미리 정해진 함수 매개 변수를 아무렇게나 당신이 원하는 대로 쉽게 사용할 수 있다. 만약 그것들이 함수들이라면 이것들은 첫번째 것의 결과가 두번째 것의 입력값이 되는 식으로 차례대로 불려질 필요가 없다.
처음부터 다듬어질 함수가 있어야 하는 다듬기와는 다르게 여기서는 부분 함수들만 있으며, 각자 하나의 특정 과업을 완수하여 더 크고 복잡한 것으로 합성되기 때문에 이것은 강력한 도구이다. 이것을 마치 레고 블록과 같은 것이라고 생각해보라. 그리고 당신이 올바른 조각들을 잡아서 맞는 순서로 합치는 한(즉, 당신이 올바른 함수를 제대로된 순서로 구성하는 한), 합성을 통해 당신이 상상한 무엇이든 만들 수 있을 것이다.
만약 당신이 리눅스를 사용해본 적이 있다면 리눅스의 CLI 도구들이 매우 정의된 패턴을 따르고 있음을 눈치챘을 것이다: 그것들은 오직 한 가지 일만 하고, 표준 입력에서만 읽어들일 수 있으며, 표준 출력으로만 결과를 내보낸다. 이렇게, 사용자가 다수의 명령어를 하나로 합성할 수 있게 해준다. 예를 들면:
$ cat myfile.txt | wc -l
이 예시에서는 하나의 파일을 일고 그 안에 있는 줄의 수를 계산한다. 그런데 만약 달리 합성되었거나 다른 명령어들과 합성되었다면 출력은 완전히 다를 수 있다. 하나의 출력값이 다른 것의 입력 값이 되는 식으로 함수를 고안했다면 마찬가지 일이 일어날 수도 있다. 이렇게도 합칠 수 있다.
const compose = (...fs) => (x) => fs.reduceRight((acc, f) => f(acc), x)
function lowercase(str) {
return str.toLowerCase()
}
function findMatchesOf(word) {
return function(str) {
let exp = new RegExp(word, 'g')
let matches = str.match(exp)
if(matches.length == 0) return ""
if(matches.length == 1) return matches[0]
return matches.join(", ")
}
}
function replace(oldStr, newStr) {
return function(str) {
let exp = new RegExp(oldStr, 'g')
return str.replace(exp, newStr)
}
}
function countWords(str) {
if(!str) return 0
return str.split(" ").length
}
let myString = "The brown FoX says: Hi there Dude! I'm brown, but also blind, so what's your color?"
const genericMatcher = compose(findMatchesOf("fox"), lowercase)
const colorChanger = compose(replace('brown', 'white'), lowercase)
const wordCounter = compose(countWords, replace("hi there dude!", "hello sir, nice to meet you."), lowercase)
console.log(genericMatcher(myString))
console.log(colorChanger(myString))
console.log(wordCounter(myString))
위의 마지막 예시를 확인해보라. 나는 스트링들을 다루는 네 개의 서로 다른 함수들을 만들었고 그것들을 세 개의 다른 것들로 합성했다. 이것들을 이리저리 짜맞추어 당신만의 행위들을 얻을 수도 있다. 그것이 합성의 묘미다.
코드를 자세히 보면 커버할만한 흥미로운 아이템들이 몇 가지 있다:
- 사실 어떤 함수들은(replace 와 findMatches) 인수들을 받아서 함수를 반환한다. 이는 그것들을 더 일반적으로 만들기 위함인데, 자바스크립트가 자기 자신을 반환하는 함수의 범위를 저장한다는 사실 덕분이다. (즉 closures) 우리는 자기 반환 해서 그 구성의 일부분으로 사용되는 함수의 "글로벌" 변수들로 이런 파라미터들을 가질 수 있다.
- compose 함수를 주목하라. 이것은 ES6의 rest 매개변수를 이용하고 있고 단순히 그것들(즉 우리가 넘기는 함수들) 위에서 반복처리 하고 있으며 그것들을 실행하여 그 결과들을 다음으로 보내주고 있다. reduceRight 의 사용은 우리가 함수들의 목록에서 오른쪽에서 왼쪽으로 합성하는 것을 확실히 해주는데 이것이 내가 lowercase를 항상 마지막으로 넣는 이유다. 그 순서를 거꾸로 하고 싶으면 그냥 reduce를 대신 사용하면 된다.
결론
함수에 대해 하루에 배울수 있는 만큼 다 한 거 같다. 안 그런가? 제대로 사용된다면 이러한 고차원 함수들과 다듬기 및 합성은 매우 강력한 도구들이다. 함수들에 대해서 생각하는 데 익숙하지 않고 클래스와 객체들로 작업하는 게 오히려 낫겠다면 이러한 기술들은 비직관적인 것 같겠지만 이것들이 선천적으로 더 복잡하거나 어려운 것은 아니다. 단지 관점을 바꾸는 문제다.
자바스크립트의 함수적 측면을 잘 받아들이고 즐기길 바란다!
이러한 도구들을 사용했던 적이 있는가? 함수적 접근을 사용하여 코드를 쓰는 것이 낫겠는가? 아니면 당신은 좀 더 객체 지향 개발자인가? 아래에 댓글을 남기면 좋겠지만 전쟁을 시작하진 않길. 여기서 정답은 없다!
그럼 다음에!
'웹개발' 카테고리의 다른 글
52가지 프론트엔드 면접 질문들 - 자바스크립트 (1) | 2023.08.21 |
---|---|
5가지 리액트 자바스크립트와 모범 사례들 추천 (0) | 2020.08.14 |
리덕스는 무슨 일을 할까? (그리고 언제 사용해야 할까?) (0) | 2020.08.08 |
2020년에 웹 개발자가 되는 방법 - 완성 가이드 (0) | 2020.08.01 |
리액트를 사용하여 마이크로프론트엔드 개발하는 방법: 단계적인 가이드 (0) | 2020.07.29 |