해당 게시글은 mostly-adequate-guide 의 시리즈들을 번역했습니다.
해당 게시글의 저작권은 mostly-adequate-guide 의 저작권인 Attribution-ShareAlike 4.0 International 을 준수합니다.
🤖 AI가 요약한 글이에요!
이 장에서는 명령형 프로그래밍과 대조되는 선언형 프로그래밍의 개념을 소개합니다.
선언형 코드는 '어떻게'가 아닌 '무엇을' 명시하며, SQL 쿼리나map
,compose
같은 함수형 기법이 좋은 예시입니다.
이러한 접근 방식은 코드의 가독성과 간결성을 높이고, 최적화 및 병렬 처리에 유리합니다.
설명을 돕기 위해 Flickr API를 사용하여 이미지를 가져와 표시하는 간단한 웹 애플리케이션 예제를 단계별로 구축합니다.
이 과정에서 부수 효과(API 호출, DOM 조작)를 식별하고 격리하는 방법과 순수 함수를 사용하여 애플리케이션 로직을 구성하는 방법을 보여줍니다.
Chapter 06: 예제 애플리케이션
선언형 코딩
이제 사고방식을 전환할 것입니다. 지금부터는 컴퓨터에게 작업을 어떻게 수행할지 지시하는 것을 멈추고, 대신 결과로 무엇을 원하는지에 대한 명세를 작성할 것입니다. 항상 모든 것을 세세하게 관리하려고 애쓰는 것보다 훨씬 스트레스가 적다는 것을 알게 될 것이라고 확신합니다.
선언형(Declarative) 코딩은 명령형(imperative) 과 반대로, 단계별 지침 대신 표현식을 작성하는 것을 의미합니다. SQL을 생각해보세요. "먼저 이것을 하고, 그다음 저것을 하라"는 없습니다. 데이터베이스에서 원하는 것을 명시하는 하나의 표현식이 있습니다. 우리는 작업을 어떻게 할지 결정하지 않습니다; 데이터베이스가 결정합니다. 데이터베이스가 업그레이드되고 SQL 엔진이 최적화될 때, 우리는 쿼리를 변경할 필요가 없습니다. 이는 우리의 명세를 해석하고 동일한 결과를 달성하는 여러 방법이 있기 때문입니다.
저를 포함한 일부 사람들에게는 선언형 코딩의 개념을 처음에는 파악하기 어려울 수 있으므로, 감을 잡기 위해 몇 가지 예를 살펴보겠습니다.
// 명령형
const makes = [];
for (let i = 0; i < cars.length; i += 1) {
makes.push(cars[i].make);
}
// 선언형
const makes = cars.map((car) => car.make);
명령형 루프는 먼저 배열을 인스턴스화해야 합니다. 인터프리터는 다음으로 넘어가기 전에 이 문장을 평가해야 합니다. 그런 다음 자동차 목록을 직접 반복하며, 카운터를 수동으로 증가시키고 명시적인 반복의 저속한 표시로 그 조각들을 우리에게 보여줍니다.
map
버전은 하나의 표현식입니다. 평가 순서가 필요하지 않습니다. map
함수가 반복하는 방식과 반환된 배열이 조립되는 방식에 많은 자유가 있습니다. 어떻게가 아닌 무엇을 명시합니다. 따라서 빛나는 선언형 띠를 두르고 있습니다.
더 명확하고 간결할 뿐만 아니라, map
함수는 마음대로 최적화될 수 있으며 우리의 소중한 애플리케이션 코드는 변경될 필요가 없습니다.
"네, 하지만 명령형 루프를 하는 것이 훨씬 빠릅니다"라고 생각하는 분들을 위해, JIT가 코드를 어떻게 최적화하는지에 대해 스스로 알아보시기를 제안합니다. 여기에 빛을 비춰줄 수 있는 훌륭한 비디오가 있습니다.
또 다른 예시입니다:
// 명령형
const authenticate = (form) => {
const user = toUser(form);
return logIn(user);
};
// 선언형
const authenticate = compose(logIn, toUser);
명령형 버전에 반드시 문제가 있는 것은 아니지만, 여전히 단계별 평가가 내장되어 있습니다. compose
표현식은 단순히 사실을 명시합니다: 인증은 toUser
와 logIn
의 합성입니다. 다시 말하지만, 이는 지원 코드 변경의 여지를 남기고 애플리케이션 코드가 높은 수준의 명세가 되도록 합니다.
위의 예에서 평가 순서가 지정되었지만 (toUser
는 logIn
전에 호출되어야 함), 순서가 중요하지 않은 많은 시나리오가 있으며 이는 선언형 코딩으로 쉽게 명시할 수 있습니다 (이에 대해서는 나중에 자세히 설명).
평가 순서를 인코딩할 필요가 없기 때문에 선언형 코딩은 병렬 컴퓨팅에 적합합니다. 이것이 순수 함수와 결합되어 FP가 병렬 미래를 위한 좋은 옵션인 이유입니다—병렬/동시성 시스템을 달성하기 위해 특별한 작업을 할 필요가 없습니다.
함수형 프로그래밍의 플리커(Flickr)
이제 선언적이고 조합 가능한 방식으로 예제 애플리케이션을 구축할 것입니다. 지금은 여전히 부수 효과를 사용하여 속임수를 쓸 것이지만, 최소화하고 순수한 코드베이스와 분리할 것입니다. Flickr 이미지를 가져와 표시하는 브라우저 위젯을 만들 것입니다. 앱의 기본 구조부터 시작하겠습니다.
HTML은 다음과 같습니다:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Flickr 앱</title>
</head>
<body>
<main id="js-main" class="main"></main>
<script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.2.0/require.min.js"></script>
<script src="main.js"></script>
</body>
</html>
그리고 main.js
스켈레톤은 다음과 같습니다:
const CDN = (s) => `https://cdnjs.cloudflare.com/ajax/libs/${s}`;
const ramda = CDN("ramda/0.21.0/ramda.min");
const jquery = CDN("jquery/3.0.0-rc1/jquery.min");
requirejs.config({ paths: { ramda, jquery } });
requirejs(["jquery", "ramda"], ($, { compose, curry, map, prop }) => {
// 앱 코드는 여기에 작성됩니다
});
Lodash나 다른 유틸리티 라이브러리 대신 Ramda를 사용하고 있습니다. 여기에는 compose
, curry
등이 포함되어 있습니다. RequireJS를 사용했는데, 과도하게 보일 수 있지만 책 전체에서 사용할 것이며 일관성이 중요합니다.
이제 준비가 끝났으니, 명세로 넘어갑시다. 우리 앱은 4가지 작업을 수행합니다:
- 특정 검색어에 대한 URL을 구성합니다.
- Flickr API를 호출합니다.
- 결과 JSON을 HTML 이미지로 변환합니다.
- 화면에 배치합니다.
위에 언급된 2가지 불순한(impure) 작업이 있습니다. 보이시나요? Flickr API에서 데이터를 가져오고 화면에 배치하는 부분입니다. 먼저 이들을 정의하여 격리합시다. 또한 쉬운 디버깅을 위해 우리의 멋진 trace
함수를 추가하겠습니다.
const Impure = {
getJSON: curry((callback, url) => $.getJSON(url, callback)),
setHtml: curry((sel, html) => $(sel).html(html)),
trace: curry((tag, x) => {
console.log(tag, x);
return x;
}),
};
여기서는 단순히 jQuery의 메서드를 커링되도록 래핑하고 인수를 더 선호하는 위치로 바꿨습니다. Impure
로 네임스페이스를 지정하여 이것들이 위험한 함수임을 알 수 있도록 했습니다. 향후 예제에서는 이 두 함수를 순수하게 만들 것입니다.
다음으로, Impure.getJSON
함수에 전달할 URL을 구성해야 합니다.
const host = "api.flickr.com";
const path = "/services/feeds/photos_public.gne";
const query = (t) => `?tags=${t}&format=json&jsoncallback=?`;
const url = (t) => `https://${host}${path}${query(t)}`;
모노이드(나중에 배울 것)나 조합기를 사용하여 URL을 포인트프리 방식으로 작성하는 화려하고 지나치게 복잡한 방법들이 있습니다. 우리는 읽기 쉬운 버전을 고수하고 일반적인 포인트풀 방식으로 이 문자열을 조립하기로 결정했습니다.
[필요에 따라 나머지 내용도 동일한 구조를 따라 계속 진행합니다.]