좌표 변환

좌표 변환

학습 연습 문제

소개

클로저는 JavaScript의 프로그래밍 패턴으로, 바깥 렉시컬 스코프에 있는 변수를 중첩된 코드 블록 안에서 사용할 수 있게 해줘요. JavaScript는 클로저를 자연스럽게 지원하기 때문에, 클로저가 무엇인지도 모른 채 자주 사용하게 돼요.

// Top-level declarations are global-scope
const dozen = 12;

{
  // Braces create a new block-scope
  // Referencing the outer variable is a closure.
  const twoDozen = dozen * 2;
}

// Because of the block-scope declaration, twoDozen is not available here.
twoDozen;
// => Uncaught ReferenceError: twoDozen is not defined

중괄호 {} 외에도 함수(그리고 클래스)도 새로운 스코프를 만들고, 값을 감쌀 수 있어요:

const dozen = 12;

// Functions create a new function-scope and block-scope.
// Referencing the outer variable here is a closure.
function nDozen(n) {
  // This is declared inside the function scope, and uses the top-level scope.
  // This works, and encloses the value 12.
  const twoDozen = dozen * 2;

  // This only uses the locally declared variable and the passed argument to the parameter `n`
  return (twoDozen / 2) * n;
}

// Because of the function-scope declaration, twoDozen is not available here.
twoDozen;
// => Uncaught ReferenceError: twoDozen is not defined

twoDozen 예제에서 보았듯이, 값은 중첩된 스코프(함수, 블록 등)에 감쌀 수 있지만, 그 문맥 밖으로 끌어낼 수는 없어요. 대부분의 경우, 현대 JavaScript에서는 값이 바깥 스코프로 새어 나가지 않도록 의도해요.

클로저로 상태를 저장하고 값을 전달하기

let이나 var처럼 변경할 수 있는 변수 선언을 사용하면 어떤 상태를 보존할 수 있어요:

let counter = 0;

// This function closure increments the counter's state in the outer lexical context.
// This way the counter can be shared between many calling contexts.
export function increment() {
  counter += 1;
  return counter;
}

increment();
// => 1

counter;
// => 1

상태를 새어 나가게 하지 않으면서 값을 감싸기

상태를 보존하기 위해 값을 감싸는 것, 그리고 감싼 값은 바깥으로 새어 나가지 않는다는 두 가지 아이디어를 합치면 비공개 값을 만들 수 있어요.

가장 흔한 방법은 어떤 상태를 감싸는 함수를 반환하는 함수를 만드는 거예요.

export function makeCounter() {
  let counter = 0;

  // This returns a new function that encloses the local variable counter
  return function increment() {
    counter += 1;
    return counter;
  };
}

// Counter did not leak
counter;
// => Uncaught ReferenceError: counter is not defined

// This creates a new counter.
// This assigns the increment function to the variable myFirstCounter.
const myFirstCounter = makeCounter();

typeof myFirstCounter;
// => function

myFirstCounter.name;
// => increment

myFirstCounter();
// => 1
myFirstCounter();
// => 2

// This creates new counter (with new, separate local state / enclosed counter variable)
const mySecondCounter = makeCounter();

mySecondCounter();
// => 1

// It does not affect the first counter.

myFirstCounter();
// => 3
Note

많은 프로그래머가 클로저를 어려운 개념이라고 느껴요. 그리고 함수에서 함수를 반환하는 것은 모든 프로그래밍 언어에서 흔한 일도 아니고, 아예 불가능한 언어도 있어요. 더 읽어 볼 자료를 원한다면, MDN의 클로저 가이드가 아주 자세해요.

지침

여러분의 디자인 회사는 주로 CSS 변환을 사용해 웹 페이지를 만들어 왔어요. 몇 번의 논의 끝에, 일부 계산을 동적으로 처리하기 위해 JavaScript를 사용하기로 결정했어요. 팀원 중에는 아직 JavaScript에 익숙하지 않은 사람도 있어서, {x, y} 좌표 쌍을 반복해서 변환하는 데 재사용할 수 있는 함수를 만들기 위해 함수 클로저를 사용하기로 했어요.

1. 좌표를 변환해요

클로저를 사용해 좌표 쌍을 반복해서 2차원 변환하는 함수를 반환하는 translate2d 함수를 구현해요.

기하학에서 평행 이동은 점, 벡터, 도형을 한 방향으로 같은 거리만큼 옮기는 것을 말해요. 모든 점에 상수를 더하는 것으로 해석할 수도 있어요.

const moveCoordinatesRight2Px = translate2d(2, 0);
const result = moveCoordinatesRight2Px(4, 8);
// result => [6, 8]

2. 좌표의 크기를 조정해요

클로저를 사용해 좌표 쌍을 반복해서 2차원으로 크기 조정하는 함수를 반환하는 scale2d 함수를 구현해요.

기하학에서 균일한 크기 조정은 벡터나 도형을 같은 방향으로 확대하거나 축소하는 것을 말해요. 모든 점에 상수(크기 조정 계수)를 곱하는 것으로 해석할 수도 있어요.

이 연습 문제에서는 양수인 크기 조정 값만 사용한다고 가정해요.

const doubleScale = scale2d(2, 2);
const result = doubleScale(6, -3);
// result => [12, -6]

3. 변환 함수를 합성해요

두 변환 함수를 결합해서 반복해서 사용할 수 있는 변환을 만들어요. 이런 방식을 흔히 _함수 합성_이라고 하는데, 첫 번째 함수 'f(x)' 의 결과가 두 번째 함수 'g(x)' 의 입력으로 사용돼요.

const moveCoordinatesRight2Px = translate2d(2, 0);
const doubleCoordinates = scale2d(2, 2);
const composedTransformations = composeTransform(
  moveCoordinatesRight2Px,
  doubleCoordinates,
);
const result = composedTransformations(0, 1);
// result => [4, 2]

4. 함수의 결과를 저장해요

memoizeTransform 함수를 구현해요. 이 함수는 _메모이제이션_할 함수를 받고, 전달받은 함수의 입력을 기억하는 새로운 함수를 반환해요. 그래야 같은 인자로 다시 호출했을 때 마지막 반환 값을 "기억"하고 한 번만 계산할 수 있어요.

메모이제이션은 _동적 계획법_에서 종종 사용돼요. 결과가 기억되기 때문에 비용이 큰 연산을 한 번만 수행할 수 있어요. 참고로, 이 연습 문제에서는 마지막 결과만 기억해요. 모든 결과를 메모이제이션하는 동적 계획법의 일부 해법과는 달라요.

const tripleScale = scale2d(3, 3);
const memoizedScale = memoizeTransform(tripleScale);

memoizedScale(4, 3); // => [12, 9], this is computed since it hasn't been computed before for the arguments
memoizedScale(4, 3); // => [12, 9], this is remembered, since it was computed already
GitHub에서 편집 링크가 새 창이나 탭에서 열려요
JavaScript Exercism

좌표 변환 문제를 시작해 볼 준비가 됐나요?

Exercism에 가입하고 JavaScript 트랙을 개념 37개연습 문제 159개, 그리고 실제 사람의 멘토링과 함께 배우고 익혀 보세요. 모두 무료예요.