먼저 알아야 할 것들

이 시리즈는 Three.js에 입문하는 초심자를 위한 튜토리얼입니다. 이 시리즈는 독자가 최소한 자바스크립트 기본 프로그래밍에 익숙하며, DOM이 무엇인지 설명할 수 있고, HTML과 자바스크립트로 DOM 요소를 조작할 수 있다는 전제 하에 작성하였습니다. 또한 <script type="module"> 태그로 ES2015 모듈을 불러올 수 있으며, CSS 셀렉터가 무엇인지도 알고, ES5, ES2015, 약간의 ES2016도 알며, 브라우저가 자바스크립트를 이벤트와 콜백으로만 실행한다는 것도 알고, 클로저가 무엇인지까지 안다고 가정했죠.

이번 글에서는 시리즈를 읽는 데 필요한 이런 기본 전제에 대해 간단히 환기하고 넘어가겠습니다.

ES2015 모듈

ES2015 모듈은 스크립트 안에서 import 키워드나, 인라인 <script type="module"> 태그로 불러올 수 있습니다. 두 가지 예시를 동시에 써보죠.

<script type="module">
import * as THREE from '../../build/three.module.js';

...

</script>

모듈의 경로는 반드시 상대 경로나 절대 경로여야 합니다. <img>, <a> 태그, CSS 경로와 달리 여기서 상대 경로란 ./이나 ../로 시작하는 경로를 말합니다.

더 자세한 것은 이 글의 마지막 부분을 참고하세요.

document.querySelectordocument.querySelectorAll

요소를 선택할 때는 document.querySelectordocument.querySelectorAll을 사용하면 됩니다. 첫 번째는 CSS 셀렉터와 일치하는 첫 번째 요소를 반환하고, 두 번째는 CSS 셀렉터와 일치하는 모든 요소를 반환하죠.

onbody를 쓰지 마세요

옛날에 개발된 많은 페이지가 body 태그의 onload 속성을 사용합니다.

<body onload="somefunction()">

이런 스타일은 더 이상 권장하지 않습니다. script 태그를 페이지의 끝에 삽입하세요.

<html>
  <head>
    ...
  </head>
  <body>
     ...
  </body>
  <script>
    // inline javascript
  </script>
</html>

또는 defer 속성을 사용하는 것이 좋습니다.

클로저(closure)란?

function a(v) {
  const foo = v;
  return function() {
     return foo;
  };
}

const f = a(123);
const g = a(456);
console.log(f());  // 123 출력
console.log(g());  // 456 출력

위 예제에서 함수 a는 매번 호출할 때마다 새로운 함수를 반환합니다. 이는 상수 foo에 대한 클로저이죠. 자세한 건 여기를 참고하기 바랍니다.

this 이해하기

this는 마법이 아닙니다. this는 다른 인자들처럼 자동으로 함수에 넘겨지는 일종의 변수이죠. 간단히 함수를 직접 호출하는 경우를 예로 들어보겠습니다.

somefunction(a, b, c);

여기서 thisnull입니다(엄격(strict) 모드나 모듈 안에서). 반면 . 연산자를 붙여 메서드로 호출하는 경우,

someobject.somefunction(a, b, c);

thissomeobject로 지정될 겁니다.

많은 사람들이 헷갈리는 부분은 메서드를 콜백 처리할 때이죠.

 const callback = someobject.somefunction;
 loader.load(callback);

자바스크립트에 능숙하지 않은 사람이 보기에는 문제가 없을지 모르나, loader.load는 콜백 함수를 . 연산자를 써서 호출하지 않으므로-loader가 별도로 this를 지정하지 않는 한-this는 null이 됩니다. 콜백 함수를 호출할 때 thissomeobject로 지정하려면 명시적으로 this를 종속시켜야(binding) 합니다.

 const callback = someobject.somefunction.bind(someobject);
 loader.load(callback);

this를 이해하기 어렵다면, this에 관한 문서를 한 번 읽어보길 권합니다.

ES5/ES2015/ES2016

var 대신 constlet 사용하기

constlet을 쓸 수 있는 환경에서 var를 써야할 이유는 전혀 없고, 지금 var를 사용하는 것은 실력 향상에 전혀 도움이 되지 않습니다. 변수를 재할당할 일이 없을 경우 const를 사용하세요. 변수를 재할당하는 것은 흔치 않은 일이니, 주로 const를 더 많이 쓰게 될 겁니다. 변수의 값을 바꿔야 한다면 let을 사용하세요. 이런 습관을 들이면 버그를 훨씬 더 많이 줄일 수 있습니다.

for(elem in collection) 대신 for(elem of collection) 사용하기

for offor in의 문제를 해결하기 위해 새로 추가된 문법입니다.

예를 들어 객체의 키/값 쌍을 반복문으로 돌릴 경우 다음과 같이 쓸 수 있죠.

for (const [key, value] of Object.entries(someObject)) {
  console.log(key, value);
}

forEach, map, filter 등을 적절히 활용하기

ES5에서 배열에 forEach, map 메서드 등이 추가되었고, ES2015에서는 filter 메서드 등 여러 유용한 메서드가 추가되었습니다.

구조분해할당(destructuring) 사용하기

const dims = { width: 300, height: 150 }. 이런 객체가 있다고 해보죠. 객체의 각 속성을 별도의 변수에 할당하고자 합니다.

기존 방법

const width = dims.width;
const height = dims.height;

새로운 방법

const { width, height } = dims;

구조분해할당은 배열에도 적용할 수 있습니다. const position = [1, 2, 3, 4]. 이런 배열이 있다고 해보죠.

기존 방법

const x = position[2];
const y = position[1];

새로운 방법

const [ , y, x ] = position;

또한 매개변수에도 구조분해할당을 적용할 수 있습니다.

const dims = { width: 300, height: 150 };
const position = [1, 2, 3, 4];

function distFromOrig([x, y]) {
  return Math.sqrt(x * x + y * y);
}

const dist = distFromOrig(position);  // dist = 2.236...

function area({ width, height }) {
  return width * height;
}
const a = area(dims);  // a = 45000

객체 선언 시 축약 문법 사용

기존 방법

 const width = 300;
 const height = 150;
 const obj = {
   width: width,
   height: height,
   area: function() {
     return this.width * this.height
   },
 };

새로운 방법

 const width = 300;
 const height = 150;
 const obj = {
   width,
   height,
   area() {
     return this.width * this.height;
   },
 };

전개 연산자 ... 사용하기

전개 연산자는 매우 유용합니다. 예를 들어보죠.

 function log(className, ...args) {
   const elem = document.createElement('div');
   elem.className = className;
   elem.textContent = args.join(' ');
   document.body.appendChild(elem);
 }

또 배열을 인자로 넘겨줄 때도 유용합니다.

const position = [1, 2, 3];
somemesh.position.set(...position);

배열을 얕은 복사할 때 사용할 수도 있고

const copiedPositionArray = [...position];
copiedPositionArray.push(4); // [1,2,3,4] 
console.log(position); // [1,2,3] 기존 배열은 영향을 받지 않음

객체를 합칠 때도 사용할 수 있죠.

const a = { abc: 123 };
const b = { def: 456 };
const c = { ...a, ...b };  // c = { abc: 123, def: 456 }

class 사용하기

ES5 이하의 문법으로 클래스 스타일의 객체를 만드는 방법은 다른 개발자들에게 낯선 요소 중 하나였습니다. ES2015부터는 C++/C#/Java 등 다른 객체지향 언어처럼 class 키워드를 사용해 클래스를 생성할 수 있습니다.

getter와 setter

모던 프로그래밍 언어에는 대부분 gettersetter가 있습니다. ES2015의 class 문법을 사용하면 훨씬 쉽게 getter와 setter를 설정할 수 있죠.

화살표 함수(arrow function) 활용하기

화살표 함수는 특히 콜백과 프로미스를 처리할 때 유용합니다.

loader.load((texture) => {
  // 불러온 텍스처를 사용
});

화살표 함수는 this 값을 지정하지 않습니다.

(※ 원문에서는 "Arrow functions bind this.(화살표 함수는 this를 바인딩한다.)"라고 적었지만, 표준에 기재된 바 화살표 함수는 익명 함수로 this, arguments, super 또는 new.target을 지정하지 않습니다. 혼돈을 막기 위해 달리 번역하였으니 참고 바랍니다. 역주. 출처: MDN)

const foo = (args) => {/* code */};

위 예제는 다음과 과 같죠.

const foo = (function(args) {/* code */}).bind(this));

프로미스(Promise)와 async/await

프로미스는 비동기 처리를 도와줍니다. async/await는 프로미스를 좀 더 쉽게 쓰도록 도와주죠.

이 둘은 짧게 다루기 어려우므로 다른 글(프로미스, async/await)을 참고하기 바랍니다.

템플릿 리터럴(Template Literals)

템플릿 리터럴은 따옴표 대신 백틱(backticks, 억음 부호)을 사용한 문자열입니다.

const foo = `템플릿 리터럴 문자열입니다`;

템플릿 리터럴은 문자열에 2가지 기능을 더한 것인데요. 하나는 여러 줄에 걸쳐 쓸 수 있는 기능입니다.

const foo = `템플릿
리터럴
문자열입니다`;
const bar = "템플릿\n리터럴\n문자열입니다";

예제의 foobar는 같은 문자열이죠.

다른 하나는 문자열 중간에 자바스크립트 표현식을 끼워넣을 수 있는 기능으로, ${ 자바스크립트 표현식 }처럼 사용합니다.

const r = 192;
const g = 255;
const b = 64;
const rgbCSSColor = `rgb(${r},${g},${b})`;

함수를 실행하거나

const color = [ 192, 255, 64 ];
const rgbCSSColor = `rgb(${ color.join(',') })`;

계산식을 넣을 수도 있습니다.

const aWidth = 10;
const bWidth = 20;
someElement.style.width = `${ aWidth + bWidth }px`;

자바스크립트 네이밍 컨벤션(coding convection, 코딩 스타일 규약) 지키기

코드를 작성하는 것은 여러분의 자유지만, 웬만하면 한 가지 규약은 지키는 것이 좋습니다. 자바스크립트의 변수, 메서드, 함수 이름은 전부 lowerCamelCase이죠. 생성자나, 클래스의 이름은 UpperCamelCase입니다. 이 규약만 따른다면 자바스크립트의 세계에서 크게 문제될 것은 없죠. 대부분의 린터(linter)나 린팅 프로그램이 위 네이밍 컨벤션을 지키지 않는 코드에 대해 에러나 경고를 던집니다.

(※ 문서에 따라 lower camel case는 camelCase, upper camel case는 PascalCase로 부르기도 합니다. 역주.)

const v = new vector(); // 모든 클래스가 대문자로 시작할 경우 에러
const v = Vector();     // 모든 함수가 소문자로 시작할 경우 에러

Visual Studio Code 사용해보기

이 세상에는 훌륭한 에디터가 많습니다. 자신이 좋아하는 에디터를 쓰는 것이 가장 좋죠. 하지만 아직 마음에 드는 자바스크립트 에디터가 없다면 Visual Studio Code를 써보세요! 에디터를 설치하고 eslint를 설정 하면-환경에 따라 시간이 좀 걸릴지 모르나-무수한 자바스크립트 코드의 버그를 잡는 데 도움이 될 겁니다.

예를 들어 no-undef 규칙을 활성화하면 VSCode는 ESLint를 통해 변수의 값이 지정되지 않은 경우 경고를 출력할 겁니다.

위 예시에서 작성자는 doTheThing 함수를 doThing로 잘못 적었습니다. 그러자 doThing 아래에 밑줄이 생겼죠. 그리고 그 위에 커서를 올리면 해당 함수를 선언하지 않았다고 알려줍니다. 미리 에러를 하나 줄인 셈이죠.

THREE를 쓸 때도 경고가 나타날 것이므로 코드 상단에 /* global THREE */를 선언해 THREE가 미리 선언되었음을 알려줄 수 있습니다.

위 이미지에서 ESLint는 대문자로 시작하는 이름은 생성자이니 new 키워드를 사용하라고 알려줍니다. new-cap 규칙 덕에 에러를 하나 더 줄였네요.

이 외에도 설정할 수 있는 규칙이 거의 100개 정도 더 있습니다. 아까 var 대신 constlet을 쓰라고 했었죠.

아래 이미지에서 작성자는 var를 썼는데, let이나 const를 쓰라는 경고를 받았습니다.

let을 쓰고 재할당을 하지 않자 const를 쓰라고 제안합니다.

물론 var가 더 좋다고 생각하면 이 규칙을 꺼버리면 됩니다. 이 규칙을 쓰는 건 제가 성능과 버그 예방 면에서 var보다 letconst를 쓰는 걸 더 좋아하기 때문이죠.

일부 파일이나 코드의 일부분에서 규칙을 덮어 씌워야 할 경우, 주석을 추가해 규칙을 끌 수 있습니다.

구형 브라우저를 지원해야 한다면 트랜스파일러를 쓰세요

대부분의 모던 브라우저가 자동 업데이트 기능을 사용하기에 최신 문법을 사용해 생산성과 버그 예방 두 마리의 토끼를 동시에 잡을 수 있습니다. 만약 꼭 구형 브라우저를 지원해야 한다면 ES5/ES2015 등의 최신 문법을 구형 문법으로 변환시켜주는 트랜스파일러를 사용하길 권장합니다.