자바스크립트의 비밀스러운 삶: 함수 합성의 힘

발행: (2025년 12월 20일 오후 12:57 GMT+9)
11 min read
원문: Dev.to

I’m ready to translate the article for you, but it looks like the text you’d like translated isn’t included in your message. Could you please paste the content you want translated (excluding the source line you already provided)? Once I have the text, I’ll translate it into Korean while preserving the original formatting, markdown, and code blocks.

Source:

문제

티모시(Timothy)는 좋은 시간을 보내고 있지 않았다. 그는 마치 프로그래밍이라기보다 교통사고를 당한 수학 방정식처럼 보이는 코드 블록을 바라보고 있었다.

“정말 싫어,” 그가 중얼거렸다.

마가렛(Margaret)이 그의 책상 옆에 서서 눈썹을 치켜올렸다.

“‘싫어’라는 말은 강한 표현이야, 티모시. 그 코드가 너에게 무슨 일을 했어?”

“거꾸로 읽히거든!” 티모시가 화면을 가리켰다. “사용자 데이터를 포맷하기 위해 멋진 헬퍼 함수를 만들었는데, 이걸 조합하는 건 악몽이야. 마치 양파를 깎는 것 같은데, 양파가 나를 울게 만들거든.”

그는 코드를 강조했다:

const userInput = "   gandalfthegrey  ";

// My helper functions
const trim      = str => str.trim();
const upperCase = str => str.toUpperCase();
const exclaim   = str => `${str}!`;
const bold      = str => `**${str}**`;

// The "Onion" of code
const onionResult = bold(upperCase(exclaim(trim(userInput))));

console.log(onionResult); // "**GANDALFTHEGREY!**"

“아,” 마가렛이 고개를 끄덕였다. “Inside‑Out 문제야. 문자열을 먼저 트림하고, 그 다음 대문자로 바꾸고, 느낌표를 추가하고, 마지막으로 굵게 표시하고 싶지? 그런데 역순으로 써야 해.”

“다른 단계도 추가하고 싶다면?” 티모시가 물었다. “그 중첩의 정확한 가운데를 찾아서 끼워 넣어야 해. 정말 지저분해.”

“그렇지,” 마가렛이 동의했다. “너는 인간을 위해서가 아니라 컴퓨터를 위해 코드를 짜고 있어. 컴퓨터는 중첩을 좋아하고, 인간은 순서를 좋아해. 우리는 그 양파를 파이프라인으로 바꿔야 해.”

개념: 파이프라인

Margaret는 마커를 집어 들고 두 접근 방식을 비교하는 간단한 다이어그램을 화이트보드에 스케치했습니다.

The "Onion" (Nested)      vs.      The Pipeline (Composed)
Reads Inside‑Out                   Reads Top‑to‑Bottom

      bold(                         "  data  "
        upperCase(                      |
          trim(                         V
            " data "               +----------+
          )                        |   trim   |
        )                          +----------+
      )                                 |
                                        V
                                   +-----------+
                                   | upperCase |
                                   +-----------+
                                        |
                                        V
                                   +----------+
                                   |   bold   |
                                   +----------+
                                        |
                                        V
                                    "**DATA**"

“데이터를 물이라고 생각해 보세요,” 라고 그녀가 다이어그램 오른쪽을 가리키며 설명했습니다. “함수 합성은 배관 작업과 같습니다. 한 함수의 출력을 바로 다음 함수의 입력에 연결합니다. 데이터가 위에서 아래로 부드럽게 흐릅니다.”

“이게 훨씬 나아 보이네요,” 라고 Timothy가 인정했습니다. “하지만 JavaScript에는 파이프가 기본적으로 없어요.”

“현재는 내장 파이프 연산자가 없어요,” 라고 Margaret가 명확히 말했습니다. “하지만 우리는 직접 그 동작을 구현할 수 있습니다. 이것이 함수형 프로그래밍의 슈퍼파워 중 하나죠.”

pipe 만들기

Margaret는 새 파일을 열었다.

“함수들의 리스트를 받아 순서대로 실행하는 함수가 필요해. 먼저 정확히 무슨 일이 일어나는지 보여주기 위해 pipeLoop라는 버전을 작성해 보자.”

// The "Boring" (but clear) Pipe
const pipeLoop = (...functions) => {
    return (initialValue) => {
        let result = initialValue;

        // Loop through every function in the list
        for (let func of functions) {
            // Pass the result of the last function into the next one
            result = func(result);
        }

        return result;
    };
};

“그냥 루프일 뿐이야,” 라고 Margaret가 설명했다. “초기값을 받아 첫 번째 함수에 넘기고, 그 결과를 다음 함수에 넘기고, 함수가 다 소진될 때까지 계속 진행하는 거야. 일종의 조립 라인이지.”

Timothy는 고개를 끄덕였다. “알겠어, result 변수를 계속 업데이트하는 거구나.”

“바로 그거야,” Margaret가 말했다. “그리고 우리는 JavaScript 개발자라서 한 줄 코드(​one‑liners​)를 좋아하니까 보통 reduce를 사용해서 이렇게 작성해. pipeLoop와 정확히 같은 일을 하지. 두 구현이 동등하니까 실제 코드에서는 더 깔끔한 pipe 버전을 사용할 거야:”

// The "Pro" Pipe
const pipe = (...functions) => (initialValue) =>
    functions.reduce(
        (currentValue, currentFunction) => currentFunction(currentValue),
        initialValue
    );

그녀는 새로운 도구를 사용해 Timothy의 복잡한 코드를 정리했다:

// Step 1: Reuse the small functions (trim, upperCase, exclaim, bold) you defined earlier.

// Step 2: Create the pipeline
const formatString = pipe(
    trim,
    upperCase,
    exclaim,
    bold
);

// Step 3: Use it
const result = formatString("   gandalfthegrey  ");
console.log(result); // "**GANDALFTHEGREY!**"

“와,” Timothy가 말했다. “정말 영어 문장처럼 읽히네. Trim, Uppercase, Exclaim, Bold. 위에서 아래로 차례대로.”

그런데 어떻게 디버깅하나요?

Timothy가 약간 찡그렸다.

“내 ‘onion’ 코드에서는 중간에 변수를 console.log 로 찍어 무슨 일이 일어나고 있는지 볼 수 있었어. 여기서는 데이터가 파이프 안에 숨겨져 있어. 볼 수가 없어.”

“우리는 창을 만들거야,” Margaret가 말하며 작은 헬퍼 함수를 작성했다:

const trace = label => value => {
    // Use comma separation so objects log correctly
    console.log(label, value);
    return value;
};

“값을 그대로 로그에 남기고 그대로 반환할 뿐이야. 이제 파이프를 깨뜨리지 않고도 데이터에 스파이할 수 있어:”

const formatString = pipe(
    trace('after trim'),   // “Okay, that is actually incredibly useful.”
);

“아하!” 연결: 왜 커링이 필요했는가

“이제 마지막 조각이야,” 마가렛이 말했다. “trim이나 upperCase는 인자를 하나만 받기 때문에 잘 동작해. 그런데 표준적인 두 인자를 받는 덧셈 함수를 쓰고 싶다면 어떨까?”

그는 예시 코드를 입력했다:

// A standard function
const standardAdd = (a, b) => a + b;
const double = x => x * 2;

// This won't work in a pipe! 
const newNumber = pipe(
    double,
    standardAdd // Problem: expect (a, b), but pipe only gives it 'a'
)(5); 

console.log(newNumber); // NaN (because 'b' was undefined!)

마가렛은 문제를 보여주기 위해 또 다른 스케치를 그렸다.

파이프는 표준 커넥터다 – 각 구간은 정확히 하나의 입력과 하나의 출력을 가져야 한다.

The Problem: A mismatched pipe connection

[Previous Func] outputs: 5
       |
       V
(Pipe expects 1 input) -> +-------------------+
                          | standardAdd(a, b) |  +-------------------+

커링을 어댑터로 사용하기

“그리고 이제 왜 지난주에 커링을 배웠는지 알겠지.”

“어댑터라고?” 티모시가 물었다.
“정확히 그렇다!” 마가렛이 답했다. “커링은 인자를 미리 채워서 함수가 하나의 인자만 남도록 만든다—그 하나는 파이프를 통해 들어오는 값이다.”

The Solution: Currying as an Adapter

[Previous Func] outputs: 5
       |
       V
+-----------------+
|    add(10)      |  b => a + b;

// We reuse 'double' from the previous example

// Now we can "pre‑load" the add function
// add(10) returns a function that waits for the second number
const processNumber = pipe(
    double,    // 5 * 2 = 10
    add(10),   // 10 comes down the pipe. 10 + 10 = 20
    double    // 20 * 2 = 40
);

console.log(processNumber(5)); // 40

Margaret의 치트 시트

Composition (파이프)를 사용할 때:

  • 데이터 변환: 원시 데이터(예: 사용자 객체)가 여러 단계의 “다듬기”( sanitize → validate → normalize )를 거쳐야 할 때.
  • 가독성: 함수가 세 층 이상 중첩(f(g(h(x))))된 경우.
  • 리팩토링: “신 함수”(God Function)가 열 가지 일을 할 때. 이를 열 개의 작은 함수로 나누고 pipe로 연결합니다.

비동기에 대한 경고:

This simple pipe function is synchronous. If your functions return Promises (i.e., they are async), the pipe will break because the next function will receive a Promise instead of a plain value.

Glossary

  • Unary Function: 정확히 하나의 인자를 받는 함수. 이상적으로 파이프 안의 모든 함수는 유니어리여야 합니다.
  • Pure Function: 같은 입력에 대해 항상 같은 출력을 반환하고 부작용(예: 전역 변수 변경)을 일으키지 않는 함수.
  • Currying: 여러 인자를 받는 함수를 단일 인자 함수들의 체인으로 변환하는 것. 복잡한 함수를 파이프에 맞게 “어댑터” 역할을 합니다.

Aaron Rose는 tech-reader.blog의 소프트웨어 엔지니어이자 기술 작가이며, Think Like a Genius의 저자입니다.

Back to Blog

관련 글

더 보기 »

JavaScript에서 함수 합성

소개: 함수 합성(Functional composition)은 함수 파이프라인(function pipelines)이라고도 하며, 간단한 함수를 연결하여 보다 가독성이 높고 모듈화된 코드를 만들 수 있게 합니다. 정의...

JavaScript에서 일급 함수

소개 개발자들이 JavaScript를 배우면서 “first‑class functions”라는 용어가 토론과 문서에서 자주 등장합니다. JavaScript에서 함수는 …