openlang

옛 CoffeeScript를 손으로 옮기는 JavaScript 연습

유행은 지났지만 생각보다 훨씬 잘 먹히는 연습이 있습니다. 배우는 사람에게 작고 오래된 CoffeeScript 파일을 하나 주고, 컴파일러를 돌리지 말고 현대 JavaScript로 다시 쓰게 하는 것입니다.

거의 모든 설탕 문법에는 현대의 짝이 있고, 바로 그게 요점입니다. 짝들이 비슷하지만 똑같지는 않기 때문입니다. @name은 this.name입니다. ==는 언제나 ===였습니다. a ? b는 a ?? b처럼 보이고 실제로도 거의 같습니다(다만 CoffeeScript 쪽은 선언된 적 없는 변수도 받아 주어 typeof 검사로 컴파일되는 반면, 선언되지 않은 이름에 ??를 쓰면 ReferenceError가 납니다. 배우는 사람이 한 번 틀려 봐야만 알게 되는 바로 그런 종류의 차이입니다). 범위, for own, 암묵적 반환, soak 연산자도 모두 이런 거의 같음의 맛을 냅니다.

결국 배우는 사람은 문법이 아니라 의미를 찾으려고 JavaScript 명세를 읽게 되고, 초보자 연습 중에 그걸 해내는 것은 별로 떠오르지 않습니다.

이어지는 대화 11

서로의 근거를 따라
  1. 고유 링크

    제가 가장 흔하게 예상하는 실수는 ?? 자리에 ||를 쓰는 거예요. 더 오래된 습관이고 대충 보면 통과하거든요. 둘은 null이나 undefined가 아닌 모든 falsy 값에서 갈려요.

    const saved = 0
    console.log(saved || 10, saved ?? 10)
    console.log('' || 'none', '' ?? 'none')

    결과는 10 0, 그다음은 none과 빈 문자열이에요. CoffeeScript의 ?는 0과 ''를 있는 값으로 봐요. ??와 같죠. 그러니까 ||로 옮기면 설정 화면이 흔히 들고 있는 바로 그 값들에서 동작이 바뀌어요. saved에 number라고 타입을 붙여도 잡히지 않아요. 0도 멀쩡한 number니까요.

  2. 고유 링크

    그런데 그 값이 입력창에서 오면 또 달라요. 수량 칸을 비우고 저장을 눌러요. input.value는 null이 아니라 ''라서, ??는 빈 문자열을 그대로 붙잡고 기본값이 있어야 할 자리에 빈칸이 떠요. ||는 10을 다시 넣어 주고요(누가 일부러 입력한 진짜 0 위에도 10을 넣어 버리겠지만요). 빈 칸이 무슨 뜻인지는 두 연산자 모두 몰라요. 그건 둘 중 어느 쪽이 끼어들기 전에, 칸을 읽는 코드에서 누군가 정해야 해요.

  3. 고유 링크

    저는 CoffeeScript가 ?에 붙인 이름인 existential을 필요 이상으로 좋아해 왔어요. 소리 내 읽으면 그게 아예 존재하느냐고 묻는 것처럼 들리는데, 선언되지 않은 변수라면 컴파일된 typeof 검사가 정말로 그걸 물어요. 선언된 변수라면 값이 null이나 undefined인지만 묻는데, 훨씬 작은 질문이 같은 거창한 단어를 입고 있는 셈이에요. JavaScript는 nullish coalescing이라는 이름을 골랐어요. 더 투박하지만 적어도 어떤 값을 말하는지는 밝히는 이름이에요.

  4. 고유 링크

    선언되지 않은 경우는 중요했습니다(파일마다 함수로 감쌌기 때문에 라이브러리가 로드됐는지 물을 때는 if jQuery?를 썼습니다). 이것을 jQuery ?? fallback으로 옮기면 jQuery가 없는 페이지에서는 예외가 납니다.

  5. 고유 링크

    저라면 그 연습에서 soak 호출을 맨 앞에 둘 거예요. CoffeeScript의 callback?()는 호출 전에 typeof callback === "function" 검사를 하도록 컴파일돼서, 문자열이나 숫자를 담은 콜백은 조용히 건너뛰어요. JavaScript의 callback?.()는 null과 undefined만 확인해요. 문자열을 주면 문자열은 호출할 수 없으니 TypeError가 나요. 두 줄은 점 하나 차이인데도요.

  6. 고유 링크

    선언된 경우라면 TypeScript가 잡아요. string | (() => void) | undefined 타입에 callback?.()를 쓰면 컴파일 오류예요. 함수라고 주장만 하는 JSON 값에는 여전히 typeof callback === 'function' 검사가 필요해요.

  7. 고유 링크

    저는 조금 더 부드럽게 하고 싶어요. coffee -c로 초안을 뽑고, 배우는 사람이 출력의 모든 줄을 설명하게 한 다음, 실제로 쓸 법한 JavaScript로 다듬게 하는 거예요. typeof 검사나 모아지는 배열은 그대로 만나지만, 답안지가 이미 페이지에 있는 셈이죠. 순수하게 손으로 옮기는 건 좋게 들리지만, ?를 잘못 짐작한 사람은 한참 뒤에 뭔가 깨질 때까지 틀렸다는 걸 알려 줄 게 없어요.

  8. 고유 링크

    답안지가 바로 문제입니다. 컴파일된 CoffeeScript는 CoffeeScript가 생각하는 JavaScript입니다(ref, j 같은 임시 변수, for own마다 붙는 hasProp 도우미, 범위 루프마다 방향을 확인하는 삼항식까지, 전부 완벽하게 옳지만 사람이 쓰는 방식으로 쓰인 것은 하나도 없습니다). 그걸 한 줄씩 설명하는 학습자는 주로 컴파일러의 사고방식을 배웁니다. 흥미롭기는 해도 배울 내용은 아닙니다. ?를 잘못 짐작하는 것이야말로 쓸모 있는 부분입니다. 대신 작은 테스트 파일을 연습과 짝지어 두면, 잘못된 짐작이 곧바로 실패하는 사례로 드러납니다.

  9. 고유 링크

    놀랄 거리는 범위에서 나올 것 같아요. 장바구니 페이지가 for i in [1..count]로 상품마다 행을 그려요. 장바구니를 비우면 count가 0이 되고, 페이지에 1번과 0번 행이 떠요. CoffeeScript 범위는 끝이 시작보다 작으면 거꾸로 세기 때문에 [1..0]은 원소가 없는 게 아니라 두 개예요. 손으로 옮기면 for (let i = 1; i <= count; i++)가 될 텐데, 이건 0에서 아무것도 안 해요. 그러니까 배우는 사람은 버그가 있었다는 걸 눈치채지도 못한 채 고치게 돼요(그게 이 연습의 최고의 결과인지 최악의 결과인지 솔직히 못 정하겠어요).

  10. 고유 링크

    그건 문서에 나온 탈출구가 있습니다. by 1을 붙이면 방향이 고정됩니다(그래서 for i in [1..count] by 1은 0에서 한 번도 돌지 않습니다). 거꾸로 세는 동작은 아무도 처음에 시험하지 않는 그 하나의 입력에서만 드러나니 잊기 쉬웠습니다. 다만 조용한 수정이야말로 배우는 사람이 소리 내어 말해 봤으면 하는 부분입니다. 옮긴 코드가 원본과 다르게 동작한다면 그 차이를 보여 주는 입력을 댈 수 있어야 하고, count = 0은 대기에 좋은 입력입니다.

  11. 고유 링크

    이번엔 corwin 편이에요, 적어도 루프에 대해서는요. 컴파일된 범위 루프는 뭔가를 설명하기 시작하기에 최악의 자리예요. 방향 확인이 조건에 1 <= ref ? j <= ref : j >= ref로 들어 있고 증가식에도 또 들어 있어서, 그걸 한 줄씩 따라가는 사람은 주로 중첩된 삼항식 읽는 법을 배우게 되고, 0일 때 무슨 일이 생기느냐는 진짜 질문은 그 밑에 묻혀요.

같은 질문의 다른 갈래

  1. round(2.5)가 2라는 걸 학습자들은 안 믿는다

    학습자들은 자꾸 round(2.5)가 3일 거라고 짐작하는데, 제가 주로 쓰는 두 언어는 다 2라고 해요. Python은 정수 2를, Julia는 2.0을 줘요. 둘 다 동점을 짝수 쪽 이웃으로 보내서 round(0.5)는…

  2. 3 + 4 * 2가 14라는 건 쉬운 쪽이다

    Smalltalk을 처음 배우는 사람은 누구나 일찍 3 + 4 * 2를 만나고 그게 14라는 말을 들어요. 이항 메시지는 산술 우선순위 없이 왼쪽에서 오른쪽으로 가니까요. 이 부분은 한 번 보여 주면 기억에 남아요(정말 그냥…

  3. (x, x)는 두 값이 같다는 뜻이 아니다

    OCaml의 (x, x)를 "두 값이 같다"로 읽는 학습자가 계속 나와요. 그런 뜻도 아니고, 애초에 허용되지도 않아요. 처음 배우는 사람은 대개 두 설명 중 하나를 떠올려요. 하나는 앞의 x가 구성 요소에 이름을 붙이고 뒤…

    답글 5ocaml