일급 함수1
May 29, 2024
쏙쏙들어오는 함수형 코딩
# 코드의 냄새: 함수 이름에 있는 암묵적 인자
함수 본문에서 사용하는 어떤 값이 함수이름에 나타난다면 함수 이름에 있는 암묵적 인자
, 즉 *코드의 냄새가 됩니다.
예를 들어 어떤 제품은 무료 배송을 해야하고, 어떤 제품은 가격을 0으로 설정해야 합니다. 개발팀은 요구사항에 맞춰 열심히 함수를 만들었습니다. 하지만 모든 함수가 비슷합니다. 아래는 개발팀이 만든 비슷하게 생긴 함수들입니다.
함수들을 살펴 봤을때 함수이름에 있는 일부가 인자처럼 동작하는 것 같습니다. 그래서 이 냄새를 함수 이름에 있는 암묵적 인자
라고 부릅니다.
냄새를 맡는 법
함수 이름에 있는 암묵적 인자 냄새는 두 가지 특징을 보입니다.
1. 함수 구현이 거의 똑같습니다.
2. 함수 이름이 구현의 차이를 만듭니다.
함수 이름에서 서로 다른 부분이 암묵적 인자입니다.
# 리팩터링: 암묵적 인자를 드러내기
암묵적 인자를 드러내기 리팩터링
은 암묵적 인자가 *일급 값이 되도록 함수에 인자를 추가합니다
잠재적 중복을 없애고, 코드의 목적을 더 잘 표현할 수 있는 방법:
- 함수 이름에 있는 암묵적 인자를 확인합니다.
- 명시적인 인자를 추가합니다.
- 함수 본문에 하드 코딩된 값을 새로운 인자로 바꿉니다.
- 함수를 호출하는 곳을 고칩니다.
리팩터링 예제
Before
function setPriceByName(cart, name, price) {var item = cart[name];var newItem = objectSet(item, 'price', price);var newCart = objectSet(cart, name, newItem);returm newCart;}cart = setPriceByName(cart, 'shoe', 13);cart = setQuantityByName(cart, 'shoe', 3);cart = setShippingByName(cart, 'shoe', 0);cart = setTaxByName(cart, 'shoe', 2.34);
setPriceByName만 봐도 함수 이름에 있는 price
가 암묵적 인자 입니다. 함수 본문에서도 price
가 사용되는걸 볼 수 있습니다. 이제 암묵적 인자를 명시적인 인자로 바꿔봅시다.
After
function setFieldByName(cart, name, field, value) {var item = cart[name];var newItem = objectSet(item, field, valaue);var newCart = objectSet(cart, name, newItem);returm newCart;}cart = setFieldByName(cart, 'price', 13);cart = setFieldByName(cart, 'quantity', 3);cart = setFieldByName(cart, 'shipping', 0);cart = setFieldByName(cart, 'tax', 2.34);
필드명을 명시적으로 바꿔 *일급 값으로 만들었습니다. 이제 암묵적인 이름은 인자로 넘길 수 있는 값이 되었습니다. 값은 변수나 배열에 담을 수 있습니다. 그래서 *일급(first-class) 이라고 부릅니다.
일급인 것과 일급이 아닌 것을 구별하기
*일급이란 변수에 할당가능해야하고, 함수에 인자로 넘길 수 있는 값을 일급 값이라고 한다.
예를들어서 숫자는 변수에 할당 가능하고, 함수의 인자로 넘길 수 있습니다. 그래서 숫자는 *일급이라고 부를 수 있습니다. 반면 +기호는 변수에 할당 할 수 없고, 함수의 인자로도 넘길 수 없습니다. 따라서 +기호는 *일급이라고 부를 수 없습니다.
function setPriceByName(cart, name, price);function setFieldByName(cart, name, field, value);
자바스크립트에서 함수명 일부를 값처럼 쓸 수 있는 방법은 없습니다. 함수명은 *일급이 아니기 때문입니다. 그래서 위의 코드에서 setFieldByName의 이름을 봤을때 함수명의 일부를 인자로 바꿔 *일급으로 만들었습니다.
필드명을 문자열로 사용하면 버그가 생기지 않을까요?
사람은 언제나 실수를 할 수 있기 때문에 문자열에 오타가 있을 수 있습니다. 이 문제를 해결하는 방법이 2가지가 있는데 하나는 컴파일 타임에 검사
하는 것과 다른 하나는 런타임에 검사
하는 것입니다.
자바스크립트는 런타임에 타입을 확인하는 언어인 동적언어이기 때문에 런타임에 올바른 필드명인지 검사해야 합니다.
아래는 런타임 검사 방법으로 필드명이 올바른지 확인하는 코드입니다:
var validItemFields = ['price', 'quantity', 'shipping', 'tax'];function setFieldByName(cart, name, field, value) {if(!validItemFields.includes(field))throw "Not a valid item field: " + "'" + field + "'.";var item = cart[name];var newItem = objectSet(item, field, value);var newCart = objectSet(cart, name, newItem);return newCart;}function objectSet(object, key, value) {var copy = Object.assign({}, object);copy[key] = value;return copy;}
일급 필드를 사용하면 API를 바꾸기 더 어렵나요?
필드명이 추상화벽 바깥에서 사용된다고해서 내부 구현이 외부에 노출 된것은 아닙니다. 만약 내부에서 정의한 필드명이 바뀐다고 해도 사용하는 사람들이 원래 필드명을 그대로 사용하게 할 수 있습니다. 내부에서 그냥 바꿔 주면 됩니다.
예를 들어 quantity
필드명이 어떤 이유로 number
라는 이름으로 바뀌었다고 생각해 봅시다. 코드 전체를 바꾸지 않고 추상화 벽 위에서 quantity
필드명을 그대로 사용하고 싶다면 내부에서 간단히 코드를 변경해주면 됩니다.
var validItemFields = ['price', 'quantity', 'shipping', 'tax', 'number'];var translations = { 'quantity': 'number' };function setFieldByName(cart, name, field, value) {if(!validItemFields.includes(field))throw "Not a valid item field: " + "'" + field + "'.";if(translations.hasOwnProperty(field))field = translations[field];var item = cart[name];var newItem = objectSet(item, field, value);var newCart = objectSet(cart, name, newItem);return newCart;}
이런 방법은 필드명이 *일급이기 때문에 할 수 있는 것입니다. 필드명이 *일급이라는 말은 객체나 배열에 담을 수 있다는 뜻입니다.
객체와 배열을 너무 많이 쓰게 됩니다
자바스크립트에서는 객체가 해시 맵과 같은 기능을 해주기 때문에 속성과 값을 잘 표현할 수 있습니다. 그래서 자바스크립트를 사용한다면 객체를 더 많이 쓴다고 느낄 수 있습니다.
데이터를 제한된 API, 즉 어떠한 인터페이스로 정의하면 데이터를 제대로 활용할 수 없습니다. 데이터가 미래에 어떤 방법으로 해석될지 미리 알 수 없기 때문에 필요할 때 알맞은 방법으로 해석 할 수 있어야 합니다.
이것이 *데이터 지향이라고 하는 중요한 원칙입니다.
어떤 문법이든 일급 함수로 바꿀 수 있습니다
앞서 +
같은 연산자는 *일급 값이 아니라고 설명했습니다. 하지만 +연산자와 같은 함수를 만들 수 있습니다
function plus(a, b) {return a + b;}
자바스크립트에선 함수는 *일급 값입니다. 위 코드는 +연산자를 *일급 값으로 만든 것입니다.
반복문 예제: 먹고 치우기
위에 있는 두 함수를 리팩터링 하는데 효율적인 단계가 있습니다.
- 코드를 함수로 감싸기
- 더 일반적인 이름으로 바꾸기
- 암묵적 인자를 드러내기
- 함수 추출하기
- 암묵적 인자를 드러내기
After
function forEach(array, f) {for(var i = 0; i < array.length; i++) {var item = array[i];f(item);}}forEach(foods , function cookAndEat(food) {cook(food);eat(food);})forEach(dishes, function clean(dish) {wash(dish);dry(dish);putAway(dish);})
forEach() 함수는 배열과 함수를 인자로 받습니다. 함수를 인자로 받으므로 forEach() 함수는 *고차 함수입니다.
forEach() 함수를 사용할때 이름이 없는 함수를 사용한게 보일텐데, 이것은 *익명함수입니다.
반복문 안에 있는 본문은 항상 다르므로 매번 반복문을 만들어야 했는데, *고차 함수를 사용하면서 반복문에서 다른 부분만 함수로 넘겨주면 코드를 재사용하면서 다른 함수를 만들수 있게 되었습니다.
# 리팩터링: 함수 본문을 콜백으로 바꾸기
함수 본문을 콜백으로 바꾸기
리팩터링으로 함수 본문에 어떤 부분을 *콜백으로 바꿉니다. 이렇게 하면 일급 함수로 어떤 함수에 동작을 전달할 수 있습니다. 이 방법은 원래 있던 코드를 *고차 함수로 만드는 강력한 방법입니다.
원래 있던 코드를 *고차 함수로 만드는 강력한 방법:
- 함수 본문에서 바꿀 부분의
앞부분
과뒷부분
을 확인합니다. - 리팩터링 할 코드를 함수로 빼냅니다.
- 빼낸 함수의 인자로 넘길 부분을 또 다른 함수로 빼냅니다.
리팩터링 예시
Before
try {saveUserData(user);} catch (error) {logToSnapErrors(error);}try {fetchProduct(productId);} catch (error) {logToSnapErrors(error);}
위 코드와 비슷한 try/catch 구문을 다음에도 계속 작성할 것 같습니다. 하지만 이러한 중복되는 작업을 리팩터링으로 없앨 수 있습니다.
After
function withLogging(f) {try {f();} catch (error) {logToSnapErrors(error);}}withLogging(function() {saveUserData(user);})withLogging(function() {fetchProduct(productId);})
용어
*코드의 냄새 : 더 큰 문제를 가져올 수 있는 코드
*일급 값(first-class value) : 언어에 있는 다른 값처럼 쓸 수 있습니다.
*데이터 지향 : 이벤트와 엔티티에 대한 사실을 표현하기 위해 일반 데이터 구조를 사용하는 프로그래밍 형식입니다.
*고차 함수(higher-order function) : 인자로 함수를 받거나 리턴값으로 함수를 리턴할 수 있는 함수를 말합니다.
*인라인 함수(inline function) : 쓰는 곳에서 바로 정의하는 함수입니다.
*익명 함수(anonymous function) : 이름이 없는 함수입니다. 익명 함수는 필요한 곳에 인라인(inline)으로 쓸 수 있습니다.
*콜백(callback) : 인자로 전달하는 함수를 콜백이라고 부릅니다. 다른곳에서는 핸들러 함수(handler function)라고도 합니다.