아래 코드를 보자.
function something(arg1: any, arg2: any) {
// do something
}
function somethingWithIndex(index: number, arg1: any, arg2: any) {
// do something with index
}
두 함수가 하는 역할이 같다고 가정해보자.
something 함수는 "어떤 것"을 하는 함수이며 somethingWithIndex 함수는 "특정 인덱스에 있는 요소에 대해 어떤 것"을 하는 함수라고 보면 된다.
위의 상황에서 something과 somethingWithIndex의 차이는 단순히 index를 받는지에 관한 차이점만 있을 것이다.
이러한 경우 기존의 something 함수를 재사용 하기 위해서는 어떻게 해야 할까?
1. 커링 (Currying)
커링은 함수형 프로그래밍에서 사용하는 개념 중 하나이다.
커링을 요약하면 다음과 같다.
- 인자를 여러 개 받는 함수를 분리하여, 인자를 하나씩만 받는 함수로 만드는 방법
- 함수를 재사용하고 리팩토링하기 쉽게 만드는 방법
위의 내용이 무엇인지 자세히 살펴보자.
function something(arg1: any, arg2: any) {
// do something
}
function somethingWithIndex(index: number, arg1: any, arg2: any) {
// do something with index
// do something
}
위의 예제를 다시 한 번 보자.
해당 예제를 구체화 한다면 아래와 같은 것이다.
function multiple(a: number, b: number) {
return a * b;
}
const test1 = multiple(2, 1); // 2
const test2 = multiple(2, 2); // 4
const test3 = multiple(2, 3); // 6
const test4 = multiple(2, 4); // 8
const test5 = multiple(2, 5); // 10
위와 같은 경우가 있다고 생각해보자.
2를 반복해서 곱하는 중인 것을 볼 수 있다.
현재는 인자가 2개밖에 없기 때문에 읽고 이해하는데 아무 문제가 없다.
하지만, 만약 함수의 인자가 여러개로 늘어나게 된다면 무엇을 의미하는지 확인하기 어려울 것이고 2에 대한 수정이 있는 경우는 해당 부분을 모두 찾아서 수정해야 할 것이다.
function multiple(a: number, b: number) {
return a * b;
}
const CONSTANT = 2;
const test1 = multiple(CONSTANT, 1); // 2
const test2 = multiple(CONSTANT, 2); // 4
const test3 = multiple(CONSTANT, 3); // 6
const test4 = multiple(CONSTANT, 4); // 8
const test5 = multiple(CONSTANT, 5); // 10
이렇게 작성한다면 매직넘버를 별도로 관리할 수 있기 때문에 조금 더 안전할 것이다.
하지만, 여전히 CONSTANT를 반복적으로 사용하는 문제점이 있다.
또한, 인자가 여러개로 늘어난다면 가독성이 좋지 않을 것이라는 문제점은 여전히 존재한다.
이것을 해결하기 위해서 커링을 적용해보면 아래와 같다.
function multiple(a: number) {
return function (b: number) {
return a * b;
};
}
const CONSTANT = 2;
const multipleWithConstant = multiple(CONSTANT);
const test1 = multipleWithConstant(1); // 2
const test2 = multipleWithConstant(2); // 4
const test3 = multipleWithConstant(3); // 6
const test4 = multipleWithConstant(4); // 8
const test5 = multipleWithConstant(5); // 10
위와 같이 수정하여 공통적인 인자를 사용하는 부분을 별도로 분리할 수 있었다.
인자를 줄일 수 있었으며 재사용하기 쉬운 코드가 되었다고 할 수 있다.
CONSTANT와 곱셈을 하는 곳에서는 multipleWithConstant를 재사용하면 된다.
2. 클로저와의 관계
위의 코드를 다시 보자.
어디서 많이 본 형태인 것 같다.
function multiple(a: number) {
return function (b: number) {
return a * b;
};
}
조금 수정을 해보면 아래와 같다.
function outer(a) {
const base = a;
return function inner(b) {
return base + b;
};
}
const CONSTANT = 2;
const test = outer(CONSTANT);
console.log(test(2)); // 4
함수 속에서 함수를 반환하며 생명주기가 종료되어 컨텍스트가 제거된 outer 함수의 변수 base를 inner가 참조하고 있다.
앞서 봤던 클로저의 형태를 하고 있다.
일반적인 함수형 언어들에서 커링이 내부적으로 구현되어 있는 경우가 있지만 JS에서는 커링이 내부적으로 구현되어있지 않다.
JS에서는 클로저를 활용하여 커링의 기능을 활용할 수 있다.
3. 응용
커링의 특징은 앞에서 봤던 대로 아래와 같다.
- 인자를 여러 개 받는 함수를 분리하여, 인자를 하나씩만 받는 함수로 만드는 방법
- 함수를 재사용하고 리팩토링하기 쉽게 만드는 방법
그렇다면 인자를 하나씩만 받는 함수로 만드는 것이 무엇을 의미할까?
const add = (a, b, c, d) => {
return a + b + c + d;
};
console.log(add(1, 2, 3, 4)); // 10
console.log(add(1, 2, 3, 5)); // 11
console.log(add(1, 2, 3, 6)); // 12
console.log(add(1, 2, 3, 7)); // 13
console.log(add(1, 2, 3, 8)); // 14
이 함수를 분리 해보자.
const add = (a) => (b) => (c) => (d) => {
return a + b + c + d;
};
const add1 = add(1);
const add2 = add1(2);
const add3 = add2(3);
console.log(add3(4)); // 10
console.log(add3(5)); // 11
console.log(add3(6)); // 12
console.log(add3(7)); // 13
console.log(add3(8)); // 14
console.log(add(1)(2)(3)(4)); // 10
위와 같이 분리할 수 있을 것이다.
만약, 한 번에 인자들을 넣고 싶다면 마지막 줄 처럼 작성하면 된다.
add(1)의 결과는 b를 인자로 받는 함수를 반환할 것이며 이러한 과정이 순차적으로 발생할 것이기 때문이다.
3-1. 리액트에서
사실 이 개념에 대해 배우게 된 것은 리액트를 사용하면서 오랫동안 고민했던 부분이었기 때문이다.
아래 에시를 보자.
interface AttackOptionsInputProps {
options: Record<string, any>;
handleTextOptionChange: (option: string, text: string) => void;
}
export default function AttackOptionsInput({ options, handleTextOptionChange }: AttackOptionsInputProps) {
return (
<>
{constants.OPTIONS.map((optionName, index) => (
<div key={option + index} className='flex items-center gap-5'>
<span className='w-32'>{optionName}</span>
<input
className='flex-grow p-1 pl-2 font-light duration-100 min-w-[560px] shadow-default focus:outline-none focus:drop-shadow-md'
placeholder='options'
type='text'
value={options[optionName] || ''}
onChange={(event) => handleTextOptionChange(optionName, event.target.value)}
/>
</div>
))}
</>
);
}
위의 코드에서 onChange 부분을 한 번 보자.
handleTextOptionChange 함수는 optionName과 input의 value를 인자로 받아서 옵션을 바꾸는 함수다.
위의 코드는 컴포넌트에 props로 전달받은 options 객체를 수정하는 컴포넌트다.
만약, 위 컴포넌트를 호출하는 곳에서 여러 options들을 배열로 관리하고 있다면 어떻게 할 수 있을까?
위의 컴포넌트에서 handleTextOptionChange는 index도 함께 인자로 전달해야 할 것이다.
(배열에서 특정 index에 해당하는 옵션을 찾은 뒤 옵션을 수정해야 하기 때문)
위의 코드를 수정하지 않고 이러한 문제점을 해결하기 위해 커링을 사용할 수 있다.
interface AttackOptionsInputProps {
options: Record<string, any>;
handleTextOptionChange: (option: string, text: string) => void;
}
function AttackOptionsInput({ options, handleTextOptionChange }: AttackOptionsInputProps) {
return (
<>
{constants.OPTIONS.map((optionName, index) => (
<div key={option + index} className='flex items-center gap-5'>
<span className='w-32'>{optionName}</span>
<input
className='flex-grow p-1 pl-2 font-light duration-100 min-w-[560px] shadow-default focus:outline-none focus:drop-shadow-md'
placeholder='options'
type='text'
value={options[optionName] || ''}
onChange={(event) => handleTextOptionChange(optionName, event.target.value)}
/>
</div>
))}
</>
);
}
function ConponentWithMultipleOptions() {
const [optionArr, setOptionArr] = useState([옵션1, 옵션2, 옵션3, ..., 옵션N]);
const handleTextOptionChangeWithIndex = (index: number) => (option: string, text: string) {
const newOptionArr = [...optionArr]; // state 복사
newOptionArr[index][option] = text; // 값을 수정
setOptionArr(newOptionArr); // setState
}
return optionArr.map((options, index) => <AttackOptionsInput options={options} handleTextOptionChange={handleTextOptionChangeWithIndex(index)} />)
}
(단순한 예시용 코드이므로 오류가 있을 수 있을 수 있음)
위에서 AttackOptionsInput은 수정하지 않고 여전히 "props로 전달된 options를 handleTestOptionChange 함수를 통해 수정한다"의 역할을 수행하고 있다.
ComponentWithMultipleOptions 컴포넌트에서 여러 옵션을 배열로 관리하고 있지만 AttackOptionsInput은 그대로 사용할 수 있다.
handleTextOptionWithIndex 함수는 커링을 적용하여 index를 인자로 받은 뒤, option과 text를 인자로 받는 함수를 반환한다.
그 결과 handleTextOptionChangeWithIndex(index) 함수에서 index는 반환되는 익명함수 (option과 text를 인자로 받는 함수)에 의해 참조되고 있으므로 GC 대상이 아니게 된다.
따라서, 내부 익명함수 (option과 text를 인자로 받는 함수)에서 index를 참조하여 사용할 수 있게 되었다.
4. 결론
JS에서 커링은 내부적으로 구현되어 있지 않으나, 클로저를 활용하여 커링 기법을 사용할 수 있다.
커링은 함수형 프로그래밍에서 중요한 개념 중 하나이며, 함수의 재사용 및 리팩토링과 관련이 있다.
다양한 활용 방안이 있으며 핵심은 클로저의 응용이라는 것이다.
(잘못된 부분이 있다면 알려주세요)
'개발 > JavaScript' 카테고리의 다른 글
[JavaScript] 클로저 (0) | 2023.09.16 |
---|---|
[JavaScript] querySelectorAll VS getElementsByClassName (0) | 2023.09.10 |
[JavaScript] 이벤트 버블링과 캡처링 (0) | 2023.09.03 |
[JavaScript] addEventListener과 onclick의 차이 (0) | 2023.09.03 |
[NodeJS] 싱글 스레드와 이벤트 루프 (0) | 2023.07.29 |