Partial Application

함수형 프로그래밍에서 콜백 함수를 더 효과적으로 활용하는 핵심 기법 중 하나가 부분 적용(Partial Application)이다.

부분 적용은 함수형 프로그래밍의 강력한 도구로, 함수의 재사용성과 조합성을 크게 향상시킨다.
커링과는 다른 접근 방식을 취하지만, 둘 다 함수를 더 작고 재사용 가능한 단위로 분해하는 데 도움이 된다.

부분 적용의 주요 이점:

  1. 코드 중복 감소: 공통 인자를 가진 함수 호출을 단순화한다.
  2. 의도 명확화: 특화된 함수 이름을 통해 코드의 의도를 명확히 한다.
  3. 조합성 향상: 함수를 더 작고 조합 가능한 단위로 분해한다.
  4. 유연성: 필요에 따라 어떤 인자든 부분 적용할 수 있다.

자바스크립트의 콜백 패턴과 함께 부분 적용을 사용하면, 보다 선언적이고 재사용 가능한 코드를 작성할 수 있다. 특히 이벤트 처리, API 호출, 데이터 변환 같은 영역에서 부분 적용은 코드 품질을 향상시키는 실용적인 도구가 된다.

부분 적용의 개념과 패턴을 이해하면, 복잡한 로직을 더 작고 관리하기 쉬운 부분으로 분해할 수 있으며, 이는 궁극적으로 더 견고하고 유지보수하기 쉬운 코드로 이어진다.

부분 적용의 기본 개념

부분 적용이란 여러 개의 인자를 받는 함수에 일부 인자를 미리 제공하여, 나머지 인자만 받는 새로운 함수를 생성하는 기법이다. 이 과정을 통해 보다 특화된 함수를 만들어낼 수 있다.

기본적인 예:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 세 개의 인자를 받는 원본 함수
function add(a, b, c) {
  return a + b + c;
}

// 부분 적용을 통해 첫 번째 인자를 고정한 새 함수 생성
function addFrom5(b, c) {
  return add(5, b, c);
}

console.log(addFrom5(10, 15)); // 30 (5 + 10 + 15)

이 예시에서 addFrom5 함수는 add 함수에 첫 번째 인자로 5를 부분적으로 적용한 결과.
이렇게 생성된 함수는 나머지 두 개의 인자만 필요로 한다.

부분 적용 구현하기

  1. 수동 부분 적용
    가장 간단한 방법은 위 예시처럼 직접 함수를 작성하는 것.
    하지만 이 방법은 각 경우마다 새로운 함수를 정의해야 하므로 비효율적일 수 있다.

  2. bind() 메서드 활용
    자바스크립트 내장 함수인 Function.prototype.bind()를 사용하면 부분 적용을 쉽게 구현할 수 있다.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    function multiply(a, b, c) {
      return a * b * c;
    }
    
    // bind를 사용한 부분 적용
    // 첫 번째 인자는 this 컨텍스트, 그 이후는 부분 적용할 인자들
    const multiplyBy2 = multiply.bind(null, 2);
    const multiplyBy2And3 = multiply.bind(null, 2, 3);
    
    console.log(multiplyBy2(3, 4));      // 24 (2 * 3 * 4)
    console.log(multiplyBy2And3(4));     // 24 (2 * 3 * 4)
    

    bind 메서드의 첫 번째 인자는 함수가 실행될 때의 this 값을 설정하며, 나머지 인자들은 함수의 인자로 부분 적용됩된다.

  3. 부분 적용 헬퍼 함수 구현
    보다 유연한 부분 적용을 위해 헬퍼 함수를 구현할 수 있다:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    
    function partial(fn, ...args) {
      return function(...moreArgs) {
        return fn(...args, ...moreArgs);
      };
    }
    
    // 사용 예시
    function greet(greeting, name, punctuation) {
      return `${greeting}, ${name}${punctuation}`;
    }
    
    const greetHello = partial(greet, "Hello");
    const greetHelloWorld = partial(greet, "Hello", "World");
    
    console.log(greetHello("John", "!"));        // "Hello, John!"
    console.log(greetHelloWorld("!"));           // "Hello, World!"
    

    이 헬퍼 함수는 원본 함수 fn과 부분 적용할 인자들 args를 받아, 나머지 인자들 moreArgs를 받는 새로운 함수를 반환한다.

  4. 특정 위치의 인자 부분 적용
    때로는 함수의 특정 위치에 있는 인자만 부분 적용하고 싶을 수 있습니다. 이를 위한 플레이스홀더 패턴을 구현할 수 있다:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    // 플레이스홀더 심볼
    const _ = Symbol('placeholder');
    
    function partialWithPlaceholders(fn, ...args) {
      return function(...moreArgs) {
        // 플레이스홀더를 새 인자로 대체
        const mergedArgs = args.map(arg => 
          arg === _ && moreArgs.length ? moreArgs.shift() : arg
        );
    
        // 남은 인자 추가
        return fn(...mergedArgs, ...moreArgs);
      };
    }
    
    // 사용 예시
    function divide(a, b, c) {
      return (a / b) / c;
    }
    
    const divideSpecific = partialWithPlaceholders(divide, 100, _, 2);
    console.log(divideSpecific(5)); // 10 (100 / 5 / 2)
    

    이 예시에서 _ 심볼은 플레이스홀더로 사용되어, 해당 위치의 인자는 나중에 제공된다.

부분 적용의 실제 활용 사례

  1. 이벤트 핸들러
    이벤트 핸들러 함수에 추가 데이터를 전달해야 할 때 부분 적용이 유용하다:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    function handleItemClick(itemId, event) {
      event.preventDefault();
      console.log(`항목 ${itemId} 클릭됨`);
      // 항목 관련 작업 수행
    }
    
    // 여러 항목에 대한 클릭 핸들러 생성
    document.getElementById('item1').addEventListener('click', 
      partial(handleItemClick, 'item1'));
    document.getElementById('item2').addEventListener('click', 
      partial(handleItemClick, 'item2'));
    

    이 패턴을 통해 이벤트 객체 외에도 추가 정보를 핸들러에 전달할 수 있다.

  2. 설정 가능한 유틸리티 함수
    기본 유틸리티 함수에 설정을 부분 적용하여 특화된 함수를 만들 수 있다:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    function fetchData(baseUrl, endpoint, params) {
      const url = `${baseUrl}/${endpoint}?${new URLSearchParams(params)}`;
      return fetch(url).then(response => response.json());
    }
    
    // API 기본 URL에 대한 부분 적용
    const fetchFromMainAPI = partial(fetchData, 'https://api.example.com');
    const fetchFromAnalyticsAPI = partial(fetchData, 'https://analytics.example.com');
    
    // 사용 예시
    fetchFromMainAPI('users', { limit: 10 })
      .then(users => console.log('사용자 목록:', users));
    
    fetchFromAnalyticsAPI('metrics', { period: 'monthly' })
      .then(metrics => console.log('분석 데이터:', metrics));
    

    이 예시에서는 API 기본 URL을 부분 적용하여 서로 다른 API 엔드포인트에 대한 특화된 함수를 만들었다.

  3. 로깅 및 디버깅
    로깅 함수에 컨텍스트 정보를 부분 적용하면 디버깅이 용이해진다:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    function log(level, context, message) {
      const timestamp = new Date().toISOString();
      console.log(`[${timestamp}] [${level}] [${context}] ${message}`);
    }
    
    // 로그 레벨 부분 적용
    const errorLog = partial(log, 'ERROR');
    const warnLog = partial(log, 'WARN');
    const infoLog = partial(log, 'INFO');
    
    // 컨텍스트까지 부분 적용
    const userServiceErrorLog = partial(log, 'ERROR', 'UserService');
    const authServiceInfoLog = partial(log, 'INFO', 'AuthService');
    
    // 사용 예시
    errorLog('Database', '연결 실패');  // [2023-11-25T12:34:56Z] [ERROR] [Database] 연결 실패
    userServiceErrorLog('사용자 데이터 로드 실패');  // [2023-11-25T12:34:56Z] [ERROR] [UserService] 사용자 데이터 로드 실패
    authServiceInfoLog('사용자 인증 성공');  // [2023-11-25T12:34:56Z] [INFO] [AuthService] 사용자 인증 성공
    

    이 패턴을 사용하면 로그 메시지에 일관된 컨텍스트 정보를 쉽게 추가할 수 있다.

고급 부분 적용 기법

부분 적용과 함수 합성

부분 적용과 함수 합성(Function Composition)을 결합하면 강력한 데이터 처리 파이프라인을 구축할 수 있다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// 함수 합성 유틸리티
function compose(...fns) {
  return function(x) {
    return fns.reduceRight((acc, fn) => fn(acc), x);
  };
}

// 부분 적용된 함수들
const add10 = partial((a, b) => a + b, 10);
const multiply2 = partial((a, b) => a * b, 2);
const subtract5 = partial((a, b) => a - b, 5);

// 함수 합성으로 파이프라인 생성
const transformValue = compose(
  subtract5,    // 5를 뺌
  multiply2,    // 2를 곱함
  add10         // 10을 더함
);

console.log(transformValue(7)); // 29 ((7 + 10) * 2 - 5)

이 예시에서는 부분 적용된 함수들을 합성하여 데이터 변환 파이프라인을 만들었다.

동적 부분 적용

함수의 인자 중 일부에만 부분 적용을 수행하고 싶을 때 동적 부분 적용 패턴을 활용할 수 있다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function partialAny(fn, ...partialArgs) {
  const placeholders = partialArgs.reduce((acc, arg, i) => {
    if (arg === _) acc.push(i);
    return acc;
  }, []);
  
  return function(...args) {
    const mergedArgs = [...partialArgs];
    
    // 플레이스홀더 위치에 새 인자 삽입
    let argIndex = 0;
    for (let i = 0; i < placeholders.length && argIndex < args.length; i++) {
      mergedArgs[placeholders[i]] = args[argIndex++];
    }
    
    // 남은 인자 추가
    const remainingArgs = args.slice(argIndex);
    
    // 최종 함수 호출
    return fn(...mergedArgs, ...remainingArgs);
  };
}

// 사용 예시
function formatMessage(sender, recipient, action, object) {
  return `${sender}${recipient}에게 ${object}${action}했습니다.`;
}

// 특정 인자만 부분 적용
const userAction = partialAny(formatMessage, "홍길동", _, "전송", _);
console.log(userAction("김철수", "메시지")); 
// "홍길동가 김철수에게 메시지를 전송했습니다."

이 패턴을 통해 함수의 특정 인자만 부분 적용하고 나머지는 호출 시점에 제공할 수 있다.

부분 적용과 프로미스 체인

비동기 코드에서도 부분 적용을 활용하여 프로미스 체인을 더 모듈화할 수 있다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 인증 토큰을 가져오는 함수
function fetchAuthToken(apiKey, username, password) {
  return fetch('https://api.example.com/auth', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Api-Key': apiKey
    },
    body: JSON.stringify({ username, password })
  })
  .then(response => response.json())
  .then(data => data.token);
}

// API 키를 부분 적용
const fetchAuthTokenWithKey = partial(fetchAuthToken, 'my-api-key-12345');

// 사용 예시
async function login(username, password) {
  try {
    const token = await fetchAuthTokenWithKey(username, password);
    console.log(`토큰 발급 성공: ${token}`);
    return token;
  } catch (error) {
    console.error('인증 실패:', error);
    throw error;
  }
}

login('user123', 'pass456')
  .then(token => {
    // 토큰을 사용하여 추가 작업 수행
  });

이 예시에서는 API 키를 부분 적용하여 인증 요청 코드를 단순화했다.

부분 적용의 성능 고려사항

부분 적용은 클로저를 활용하므로 몇 가지 성능 관련 고려사항이 있다:

  1. 메모리 사용
    부분 적용된 함수는 클로저를 통해 적용된 인자를 메모리에 유지한다.
    많은 수의 부분 적용 함수를 생성하면 메모리 사용량이 증가할 수 있다.

  2. 실행 성능
    부분 적용은 함수 호출 계층을 추가하므로 약간의 성능 오버헤드가 발생할 수 있다. 그러나 대부분의 경우 이 차이는 무시할 만한 수준이다.

  3. 최적화 전략
    성능이 중요한 상황에서는 다음과 같은 전략을 고려할 수 있다:

    1
    2
    3
    4
    5
    6
    7
    
    // 함수 참조를 재사용하여 메모리 절약
    const commonPartial = partial(expensiveOperation, sharedConfig);
    
    // 핫 경로에서는 인라인 함수 사용
    function hotPathFunction(a, b) {
      return directFunction(fixedValue, a, b);
    }
    

함수형 라이브러리에서의 부분 적용

실제 프로젝트에서는 Lodash, Ramda와 같은 함수형 라이브러리가 제공하는 부분 적용 유틸리티를 활용할 수 있다:

Lodash

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const _ = require('lodash');

function greet(greeting, name) {
  return `${greeting}, ${name}!`;
}

// 부분 적용
const sayHello = _.partial(greet, 'Hello');
console.log(sayHello('World')); // "Hello, World!"

// 플레이스홀더 사용
const greetJohn = _.partial(greet, _, 'John');
console.log(greetJohn('Hi')); // "Hi, John!"

Ramda

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const R = require('ramda');

function formatName(first, last, title) {
  return `${title} ${first} ${last}`;
}

// 부분 적용
const formatWithMr = R.partial(formatName, [R.__, R.__, 'Mr.']);
console.log(formatWithMr('John', 'Doe')); // "Mr. John Doe"

// 다른 위치의 인자 부분 적용
const formatJohnWithTitle = R.partial(formatName, ['John', 'Doe']);
console.log(formatJohnWithTitle('Dr.')); // "Dr. John Doe"

타입스크립트에서의 부분 적용

타입스크립트에서 부분 적용 함수를 타입 안전하게 구현할 수 있다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
type PartialFunction<T extends any[], R, U extends any[]> = 
  (...args: U) => R;

function partial<T extends any[], R, U extends any[]>(
  fn: (...args: [...T, ...U]) => R,
  ...appliedArgs: T
): PartialFunction<T, R, U> {
  return (...restArgs: U) => {
    return fn(...appliedArgs, ...restArgs);
  };
}

// 사용 예시
function concat(a: string, b: string, c: string): string {
  return a + b + c;
}

const prefixAB = partial(concat, "A", "B");
const result = prefixAB("C"); // "ABC"

이 구현은 타입 안전성을 보장하며, 부분 적용 후 남은 인자의 타입도 올바르게 유지합니다.

부분 적용의 실용적 사용 패턴

  1. 설정 객체 처리
    복잡한 설정 객체가 필요한 함수에 기본 설정을 부분 적용할 수 있다:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    function createWidget(container, options, data) {
      const mergedOptions = { ...defaultOptions, ...options };
      // 위젯 생성 로직
      const widget = new Widget(container, mergedOptions);
      widget.setData(data);
      return widget;
    }
    
    // 기본 옵션으로 부분 적용
    const createDefaultWidget = partial(createWidget, _, { 
      theme: 'light',
      animation: true,
      responsive: true
    });
    
    // 사용
    const userWidget = createDefaultWidget('#user-container', userData);
    const statsWidget = createDefaultWidget('#stats-container', statsData);
    
  2. 리덕스(Redux) 액션 생성자
    Redux와 같은 상태 관리 라이브러리에서 액션 생성자를 부분 적용할 수 있다:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    // 기본 액션 생성자
    function createAction(type, payload, meta) {
      return { type, payload, meta };
    }
    
    // 특정 액션 타입에 대해 부분 적용
    const createUserAction = partial(createAction, 'USER');
    const createAuthAction = partial(createAction, 'AUTH');
    
    // 사용 예시
    store.dispatch(createUserAction({ id: 123, name: 'John' }, { source: 'api' }));
    store.dispatch(createAuthAction({ token: 'abc123' }, { timestamp: Date.now() }));
    
  3. 테스트 유틸리티
    테스트 코드에서 반복적인 설정을 부분 적용하여 코드 중복을 줄일 수 있다:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    function createTestUser(overrides, testEnv, dbClient) {
      const defaultUser = {
        username: 'testuser',
        email: 'test@example.com',
        role: 'user',
        created: new Date()
      };
    
      const user = { ...defaultUser, ...overrides };
      return dbClient[testEnv].users.create(user);
    }
    
    // 테스트 환경과 DB 클라이언트 부분 적용
    const createTestUserForDev = partial(createTestUser, _, 'development', dbClient);
    const createAdminForDev = partial(createTestUser, { role: 'admin' }, 'development', dbClient);
    
    // 테스트에서 사용
    it('should allow admin to access dashboard', async () => {
      const admin = await createAdminForDev({ username: 'admin1' });
      // 테스트 로직
    });
    

참고 및 출처