abonglog logoabonglog

Chapter 02: First Class Functions [번역] 의 썸네일

Chapter 02: First Class Functions [번역]

mostly-adequate-guide
프로필 이미지
yonghyeun

해당 게시글은 mostly-adequate-guide 의 시리즈들을 번역했습니다.

해당게시글의 저작권은 mostly-adequate-guide 의 저작권인 Attribution-ShareAlike 4.0 International 을 준수합니다.

🤖 AI가 요약한 글이예요 !
이 글은 함수형 프로그래밍에서 일급 함수(first-class functions) 의 중요성을 설명합니다.
함수를 다른 데이터 타입처럼 변수에 할당하고, 인자로 전달하며, 배열에 저장할 수 있음을 강조합니다.
불필요한 함수 래핑(wrapping)이 코드 중복과 유지보수 어려움을 야기한다고 지적하며,
함수를 직접 전달하는 간결한 방식이 더 효율적이고 재사용 가능함을 예시를 통해 보여줍니다.
또한, this 키워드 사용 시 발생할 수 있는 문제점을 언급하며 함수형 스타일에서는 이를 피하는 것이 좋다고 조언합니다.

챕터 02: 일급 함수

간단한 복습

함수가 "일급(first class)" 이라고 말할 때, 이는 함수가 다른 모든 것들과 마찬가지라는 의미입니다... 즉, 일반적인 클래스와 같다는 뜻입니다. 우리는 함수를 다른 데이터 타입처럼 다룰 수 있으며, 함수에 대해 특별히 다른 점은 없습니다 - 배열에 저장될 수도 있고, 함수 매개변수로 전달될 수도 있으며, 변수에 할당될 수도 있습니다.

이것은 자바스크립트 101 수준의 내용이지만, 깃허브에서 간단한 코드 검색만 해봐도 이 개념에 대한 집단적인 회피, 혹은 광범위한 무지를 드러내기 때문에 언급할 가치가 있습니다. 가짜 예시를 들어볼까요? 그러죠.

불필요한 함수 래핑 예시
const hi = (name) => `Hi ${name}`;
const greeting = (name) => hi(name);

여기서 greeting 함수 내에서 hi 함수를 감싸는 것은 완전히 중복됩니다. 왜일까요? 자바스크립트에서 함수는 호출 가능(callable) 하기 때문입니다. hi 뒤에 ()가 붙으면 실행되어 값을 반환합니다. 그렇지 않으면 변수에 저장된 함수 자체를 반환합니다. 확실히 하기 위해 직접 확인해 보세요:

함수 참조와 함수 호출 비교
hi; // name => `Hi ${name}`
hi("jonas"); // "Hi jonas"

greeting 함수는 단지 hi 함수를 동일한 인수로 호출할 뿐이므로, 간단히 다음과 같이 작성할 수 있습니다:

일급 함수를 사용한 간결한 할당
const greeting = hi;
greeting("times"); // "Hi times"

다시 말해, hi는 이미 하나의 인수를 기대하는 함수인데, 왜 굳이 동일한 인수로 hi를 호출하는 또 다른 함수로 감싸는 걸까요? 전혀 말이 되지 않습니다. 마치 7월 한복판에 가장 두꺼운 파카를 입고 에어컨을 틀면서 아이스 롤리를 달라고 하는 것과 같습니다.

단순히 평가를 지연시키기 위해 함수를 다른 함수로 감싸는 것은 불쾌할 정도로 장황하며, 나쁜 관행입니다 (왜 그런지는 잠시 후에 보겠지만, 유지보수와 관련이 있습니다).

다음으로 넘어가기 전에 이에 대한 확실한 이해가 중요하므로, npm 패키지 라이브러리에서 발굴한 몇 가지 재미있는 예제를 더 살펴보겠습니다.

AJAX 호출에서의 함수 래핑 비교
// 무지한 방식
const getServerStuff = (callback) => ajaxCall((json) => callback(json));
 
// 깨달은 방식
const getServerStuff = ajaxCall;

세상에는 정확히 이런 식의 ajax 코드가 널려 있습니다. 두 방식이 동일한 이유는 다음과 같습니다:

AJAX 예제 리팩토링 과정
// 이 줄은
ajaxCall((json) => callback(json));
 
// 이 줄과 같습니다
ajaxCall(callback);
 
// 따라서 getServerStuff를 리팩토링하면
const getServerStuff = (callback) => ajaxCall(callback);
 
// ...이는 다음과 동일합니다
const getServerStuff = ajaxCall; // <-- 보세요 엄마, ()가 없어요!

그리고 여러분, 이것이 바로 올바른 방식입니다. 제가 왜 이렇게 끈질기게 구는지 이해할 수 있도록 한 번 더 살펴보겠습니다.

중복 호출이 있는 컨트롤러 예시
const BlogController = {
  index(posts) {
    return Views.index(posts);
  },
  show(post) {
    return Views.show(post);
  },
  create(attrs) {
    return Db.create(attrs);
  },
  update(post, attrs) {
    return Db.update(post, attrs);
  },
  destroy(post) {
    return Db.destroy(post);
  },
};

이 우스꽝스러운 컨트롤러는 99%가 군더더기입니다. 다음과 같이 다시 작성할 수 있습니다:

일급 함수를 사용해 리팩토링된 컨트롤러
const BlogController = {
  index: Views.index,
  show: Views.show,
  create: Db.create,
  update: Db.update,
  destroy: Db.destroy,
};

... 또는 Views와 Db를 함께 묶는 것 외에는 아무것도 하지 않으므로 완전히 폐기할 수도 있습니다.

왜 일급 함수를 선호해야 할까요?

자, 이제 일급 함수를 선호해야 하는 이유에 대해 알아봅시다. getServerStuffBlogController 예제에서 보았듯이, 아무런 부가 가치 없이 불필요한 간접 계층을 추가하고 유지보수 및 검색해야 할 중복 코드의 양만 늘리기 쉽습니다.

또한, 이렇게 불필요하게 감싸진 함수를 변경해야 한다면, 래퍼 함수도 함께 변경해야 합니다.

래퍼 함수를 사용한 함수 호출
httpGet("/post/2", (json) => renderPost(json));

만약 httpGet이 발생 가능한 err를 보내도록 변경된다면, 우리는 돌아가서 "접착제" 코드도 변경해야 합니다.

래퍼 함수 수정 필요성 예시
// 애플리케이션의 모든 httpGet 호출로 돌아가서 명시적으로 err를 전달해야 합니다.
httpGet("/post/2", (json, err) => renderPost(json, err));

만약 우리가 이것을 일급 함수로 작성했다면, 변경해야 할 부분이 훨씬 적었을 것입니다:

일급 함수를 사용한 간결한 호출
// renderPost는 httpGet 내부에서 원하는 만큼의 인수로 호출됩니다.
httpGet("/post/2", renderPost);

불필요한 함수를 제거하는 것 외에도, 우리는 인수의 이름을 지정하고 참조해야 합니다. 이름은 약간의 문제가 있습니다. 특히 코드베이스가 오래되고 요구사항이 변경됨에 따라 잘못된 이름이 사용될 가능성이 있습니다.

동일한 개념에 대해 여러 이름을 사용하는 것은 프로젝트에서 혼란의 흔한 원인입니다. 또한 일반적인 코드의 문제도 있습니다. 예를 들어, 다음 두 함수는 정확히 동일한 작업을 수행하지만, 하나는 훨씬 더 일반적이고 재사용 가능하게 느껴집니다:

특정 이름과 일반 이름의 함수 비교
// 현재 블로그에 특화된 코드
const validArticles = articles =>
  articles.filter(article => article !== null && article !== undefined),
 
// 미래 프로젝트에 훨씬 더 관련성이 높은 코드
const compact = xs => xs.filter(x => x !== null && x !== undefined);

특정 이름을 사용함으로써, 우리는 겉보기에 특정 데이터(이 경우 articles)에 얽매이게 되었습니다. 이것은 꽤 자주 발생하며 많은 재발명의 원인이 됩니다.

객체 지향 코드와 마찬가지로, this가 당신의 목덜미를 물어뜯을 수 있다는 점을 인지해야 합니다. 만약 기본 함수가 this를 사용하고 우리가 그것을 일급 함수로 호출한다면, 우리는 이 누수 추상화의 분노에 취약해집니다.

this 키워드 사용 시 주의점과 해결 방법
const fs = require("fs");
 
// 무서운 방식
fs.readFile("freaky_friday.txt", Db.save);
 
// 덜 무서운 방식
fs.readFile("freaky_friday.txt", Db.save.bind(Db));

자신에게 바인딩된 Db는 자유롭게 프로토타입의 쓰레기 코드에 접근할 수 있습니다. 저는 더러운 기저귀처럼 this 사용을 피합니다. 함수형 코드를 작성할 때는 정말 필요하지 않습니다. 그러나 다른 라이브러리와 상호 작용할 때는 우리 주변의 미친 세상에 순응해야 할 수도 있습니다.

어떤 사람들은 속도 최적화를 위해 this가 필요하다고 주장할 것입니다. 만약 당신이 마이크로 최적화 부류라면, 이 책을 닫아주세요. 환불받을 수 없다면, 아마도 더 까다로운 것으로 교환할 수 있을 것입니다.

이제 다음으로 넘어갈 준비가 되었습니다.