Skip to content

제6장: 예제 애플리케이션

선언적 코딩

이제 우리의 사고방식을 전환하겠습니다. 지금부터는 컴퓨터에게 작업 방법을 일일이 지시하는 대신, 원하는 결과에 대한 명세서를 작성할 것입니다. 세밀하게 통제하려는 방식보다 이 방법이 훨씬 덜 부담스러울 것이라 확신합니다.

명령형(Imperative)과 대조되는 선언적(Declarative) 접근은 단계별 지시 대신 표현식 중심의 코드 작성 방식을 의미합니다.

SQL을 예로 들어보겠습니다. '먼저 이것을 하고 다음에 저것을 하라'는 개념이 없습니다. 데이터베이스에 요청할 내용을 단 하나의 표현식으로 명시합니다. 작업 방식을 개발자가 결정하지 않으며, 데이터베이스 엔진이 알아서 처리합니다. 데이터베이스가 업그레이드되고 SQL 엔진이 최적화되어도 쿼리를 변경할 필요가 없습니다. 이는 동일한 결과를 얻기 위해 명세를 해석하는 다양한 방법이 존재하기 때문입니다.

필자를 포함한 많은 분들이 선언적 코딩 개념을 처음 접하면 이해하기 어려울 수 있으니 몇 가지 예시를 들어 설명하겠습니다.

js
// imperative
const makes = [];
for (let i = 0; i < cars.length; i += 1) {
  makes.push(cars[i].make);
}

// declarative
const makes = cars.map(car => car.make);

명령형 루프는 먼저 배열을 초기화해야 합니다. 인터프리터는 다음 문장을 실행하기 전에 이 과정을 평가해야 하며, cars 리스트를 직접 순회하면서 카운터를 수동으로 증가시키며 노골적 방식의 명시적 반복 작업을 수행합니다.

map 버전은 단일 표현식으로 구성됩니다. 실행 순서를 지정할 필요가 없으며, 맵 함수의 순회 방식과 결과 배열 조립 방법에 자유도가 높습니다. 방법이 아닌 목적을 명시하므로 선언적 코딩의 정수를 보여줍니다.

명료성과 간결성 외에도 맵 함수는 상황에 맞게 최적화할 수 있으며 애플리케이션 코드를 변경할 필요가 없는 이점이 있습니다.

'명령형 루프가 훨씬 더 빠르다'고 생각하는 분들을 위해 JIT 컴파일러의 코드 최적화 방식을 연구해보길 권장합니다. 이에 대한 훌륭한 강의 영상이 있습니다: https://www.youtube.com/watch?v=g0ek4vV7nEA

추가 예시를 살펴보겠습니다.

js
// imperative
const authenticate = (form) => {
  const user = toUser(form);
  return logIn(user);
};

// declarative
const authenticate = compose(logIn, toUser);

명령형 버전에 문제는 없지만 여전히 단계별 실행 순서가 암묵적으로 내재되어 있습니다. compose 표현식은 단순히 '인증(Authentication)은 toUserlogIn의 조합'이라는 사실을 기술합니다. 이 접근법은 지원 코드 변경에 대한 유연성을 제공하며 애플리케이션 코드를 높은 수준의 명세서로 진화시킵니다.

위 예제에서는 실행 순서가 명시되어 있지만(toUser를 먼저 호출해야 함), 다양한 시나리오에서 실행 순서는 중요하지 않으며 선언적 코딩을 통해 이를 쉽게 명시할 수 있습니다(추후 자세히 설명).

실행 순서를 코딩할 필요가 없으므로 선언적 코딩은 병렬 컴퓨팅에 적합합니다. 이 특성은 순수 함수와 결합되어 함수형 프로그래밍이 병렬 처리 환경에서 우수한 선택지로 부각되는 이유입니다. 병렬/동시성 시스템을 구현하기 위해 특별한 작업이 필요하지 않기 때문입니다.

함수형 프로그래밍으로 구현한 플리커(Flickr) 앱

이제 선언적이고 조합 가능한 방식의 예제 애플리케이션을 제작하겠습니다. 현재는 부수 효과(side effect)를 사용하지만, 이를 최소화하고 순수 코드베이스와 분리할 것입니다. Flickr 이미지를 수집하여 표시하는 브라우저 위젯을 제작할 예정입니다. 앱 구조를 설계하며 시작해보죠. HTML 코드는 다음과 같습니다:

html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Flickr App</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 스켈레톤 코드입니다:

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 }) => {
  // app goes here
});

로대시(lodash) 대신 ramda를 사용합니다. compose, curry 등이 포함되어 있으며, requirejs를 사용해 일관성을 유지했습니다. 일관성이 가장 중요하기 때문입니다.

구조가 설정되었으므로 명세를 살펴보겠습니다. 우리 앱은 다음 4가지 기능을 수행합니다:

  1. 검색어에 특화된 URL 구성
  2. Flickr API 호출 실행
  3. 결과 JSON을 HTML 이미지로 변환
  4. 화면에 렌더링

위 기능 중 2개의 순수하지 않은(Impure) 작업이 존재합니다. Flickr API 데이터 수신과 화면 출력 작업입니다. 이들을 격리하기 위해 먼저 정의하고, 디버깅을 위한 trace 함수를 추가하겠습니다.

js
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을 구성해야 합니다.

js
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)}`;

모노이드(Monoid)나 컴비네이터 조합을 이용한 고급 기법도 있지만, 가독성을 고려하여 포인트 프리 방식 대신 일반적인 방식으로 문자열을 조립했습니다.

API 호출과 화면 출력을 수행하는 앱 함수를 작성하겠습니다.

js
const app = compose(Impure.getJSON(Impure.trace('response')), url);
app('cats');

url 함수를 호출한 후 trace와 부분 적용된 getJSON에 문자열을 전달합니다. 앱 실행 시 콘솔에서 API 응답을 확인할 수 있습니다.

console response

JSON 데이터에서 이미지 태그를 생성해야 합니다. mediaUrlsitems 배열 내 각 media 객체의 m 속성에 위치합니다.

Ramda의 범용 접근자 함수 prop을 사용하여 중첩 속성에 접근합니다. 다음은 동작 원리를 설명하기 위한 자체 구현입니다:

js
const prop = curry((property, object) => object[property]);

실제 구현은 매우 간단합니다. 객체 속성에 접근하기 위해 [] 문법을 사용합니다. 이를 활용해 mediaUrls를 추출하겠습니다.

js
const mediaUrl = compose(prop('m'), prop('media'));
const mediaUrls = compose(map(mediaUrl), prop('items'));

items 수집 후 각 mediaUrl을 추출하기 위해 map을 적용합니다. 결과적으로 mediaUrl 배열을 획득하며, 이를 화면 출력 기능과 연동하겠습니다.

js
const render = compose(Impure.setHtml('#js-main'), mediaUrls);
const app = compose(Impure.getJSON(render), url);

mediaUrls를 호출하고 <main> HTML에 렌더링하는 새로운 합성 함수를 제작했습니다. 가공되지 않은 원시 JSON 대신 렌더링 가능한 데이터가 준비되었으므로 trace 대신 render를 사용합니다.

마지막 단계는 mediaUrls을 실제 이미지 태그로 변환하는 작업입니다. 대규모 애플리케이션이라면 Handlebars/React 같은 템플릿 엔진을 사용하겠지만, 여기서는 jQuery로 img 태그를 생성합니다.

js
const img = src => $('<img />', { src });

jQuery의 html 메소드는 태그 배열을 처리할 수 있습니다. mediaUrls을 이미지 태그로 변환한 후 setHtml에 전달하면 됩니다.

js
const images = compose(map(img), mediaUrls);
const render = compose(Impure.setHtml('#js-main'), images);
const app = compose(Impure.getJSON(render), url);

모든 작업이 완료되었습니다!

cats grid

최종 구현 스크립트: include

이제 결과를 살펴보세요. '방법'이 아닌 '정의'를 기술한 아름다운 선언적 명세서입니다. 각 코드 라인은 유효한 특성을 가진 방정식으로 볼 수 있으며, 이를 통해 애플리케이션 논리를 분석하고 리팩토링할 수 있습니다.

원칙에 입각한 리팩토링

최적화 가능한 부분이 존재합니다: 아이템을 매핑하여 mediaUrl로 변환한 후, 다시 매핑하여 이미지 태그를 생성합니다. 맵과 합성(composition)에 관한 법칙이 적용 가능합니다:

js
// map's composition law
compose(map(f), map(g)) === map(compose(f, g));

이 법칙을 활용하여 코드를 최적화하겠습니다. 원칙에 따른 리팩토링을 진행합니다.

js
// original code
const mediaUrl = compose(prop('m'), prop('media'));
const mediaUrls = compose(map(mediaUrl), prop('items'));
const images = compose(map(img), mediaUrls);

맵 호출을 정렬합니다. 순수성과 방정식 추론을 통해 mediaUrls 호출을 인라인 처리할 수 있습니다.

js
const mediaUrl = compose(prop('m'), prop('media'));
const images = compose(map(img), map(mediaUrl), prop('items'));

맵 호출을 정렬했으므로 합성 법칙을 적용합니다.

js
/*
compose(map(f), map(g)) === map(compose(f, g));
compose(map(img), map(mediaUrl)) === map(compose(img, mediaUrl));
*/

const mediaUrl = compose(prop('m'), prop('media'));
const images = compose(map(compose(img, mediaUrl)), prop('items'));

이제 단일 루프에서 모든 아이템을 이미지 태그로 변환합니다. 가독성을 위해 함수를 추출하겠습니다.

js
const mediaUrl = compose(prop('m'), prop('media'));
const mediaToImg = compose(img, mediaUrl);
const images = compose(map(mediaToImg), prop('items'));

요약

실제 애플리케이션에서 새로운 기술을 적용하는 방법을 살펴보았습니다. 수학적 프레임워크를 활용하여 코드를 분석하고 리팩토링했습니다. 오류 처리와 분기 로직, 순수성 보장, 안전하고 표현력 있는 코드 작성 방법은 2부에서 다룰 예정입니다.

제7장: Hindley-Milner와 나