Waffle-Skile.github.io

JavaScript

Prerequisite

개발 환경

JavaScript

기초 문법

해당 자료는 프로그래밍 언어의 기초 문법(조건문, 반복문, 함수)에 대해서 이해하고 있음을 전제하고 보다 심화적인 문법에 대해 다루고 있습니다. (특히 C) 코딩에 처음 접하시는 경우 (References)[#references]의 생활코딩 강의를 먼저 공부하고 오시는 것을 권장합니다.

JavaScript는 웹 브라우저에서 널리 사용되는 스크립트 언어이지만, JavaScript 그 자체로도 프로그래밍 언어 입니다. 따라서 웹 페이지를 제어하는 스크립트를 다루기 이전에 JavaScript의 프로그래밍 언어적 특성을 먼저 다루고자 합니다.

강의 자료에 있는 JS 소스 코드를 실행시키는 방법에는 다음과 같이 여러 가지가 있습니다.

  1. Node.js 필요
    • 명령 프롬프트 창에서 node를 치면 JavaScript 코드를 한 줄씩 실행시킬 수 있는 셸이 실행됩니다.
    • *.js 파일에 코드를 작성하고 명령 프롬프트 창에서 node [파일_이름].js를 입력하면 해당 파일 내 전체 코드가 실행된 결과가 출력됩니다.
  2. 웹 브라우저 필요
    • 개발자 도구의 콘솔(Chrome의 경우, F12 - Console 탭)에는 JS 코드를 한 줄씩 실행시킬 수 있는 셸이 있습니다.
    • JS 코드를 <script> 태그로 감싼 HTML 파일을 작성하고 웹 브라우저로 해당 페이지를 엽니다.
    • JS 코드를 *.js 파일에 작성하고, <head><script type="text/javascript" src="[파일_이름].js"></script> 태그를 넣은 HTML 파일을 작성해 해당 웹 페이지를 엽니다.

'use strict';

변수와 상수

let, const

console.log(x);  // Reference Error

let x;
console.log(x);  // undefined

let y = 3, $_$;
x = 3 * y;
console.log(x);  // 9
console.log($_$);  // undefined

const AGE = 20;
AGE = 7;  // TypeError

const UNDEF;  // SyntaxError

var 또한 let과 유사하게 변수를 초기화시키는 기능을 담당합니다. (let 키워드는 ES6부터 지원합니다.) 하지만 var의 특성인 Hoisting이 입문자에게 혼란을 불러올 수 있기 때문에 호환 목적을 제외하고는 쓰지 않기를 권장합니다.

JS에서는 식별자(변수/상수) 이름에 $, _ 라는 문자도 사용할 수 있습니다.

Data Type

Number
console.log(typeof(1));  // "number"
console.log(typeof(1.5));  // "number"
console.log(typeof(0xff));  // "number"

max = Number.MAX_SAFE_INTEGER;
console.log(max);  // 9007199254740991
console.log((3*max + 1) - (3*max)); // 0

console.log(1 / 0);  // Infinity
console.log(0 / 0);  // NaN
console.log(1 / -0);  // -Infinity
String
console.log(typeof("Hello"));  // string
console.log('"I\'m Jeongmin Lee"');  // "I'm Jeongmin Lee"

let x = 7;
console.log(`${x} * ${x} = ${x*x}`);  // 7 * 7 = 49
console.log(x + " * " + x + " = " + x*x);  // ES5 style

console.log("Hello, World!".length);  // 13
console.log('"' + "\t  \t  2147483647\t\t   \n".trim() + '"');  // "2147483647"

Null, Undefined

console.log(typeof(null));  // object
console.log(typeof(undefined));  // undefined

Boolean

x new Boolean(x)
false false
true true
0 false
NaN false
Infinity true
undefined false
null false
"" false
"something" true
"false" true
[] true
{} true
조건 연산자

Object

Python의 dictionary와 유사한 개념입니다. Object는 여러개의 key-value 쌍을 가질 수 있으며 이 각각을 속성(Property) 이라고 부릅니다. key는 식별자여야하지만 value는 number부터 시작해서 또다른 Object나 함수까지 대부분의 표현식을 담을 수 있습니다.

const circle = {
  x: 1.2,
  y: 1.7,
  radius: 7,
}

console.log(circle);  // { x: 1.2, y: 1.7, radius: 7 }
console.log(circle.x);  // 1.2
console.log(circle["y"]);  // 1.7
circle.y *= 2;
console.log(circle.y);  // 3.4

delete circle.radius;
console.log(circle.radius);  // undefined
console.log(Object.keys(circle));  // [ 'x', 'y' ]
console.log("x" in circle);  // true
console.log("radius" in circle);  // false

const another_circle = { x: circle.x, y: circle.y }
console.log(circle);  // { x: 1.2, y: 3.4 }
console.log(another_circle);  // { x: 1.2, y: 3.4 }
console.log(circle == another_circle);  // false

코드 첫 문단을 보면 마지막 속성을 나열할 때에도 ,를 붙이는 걸 볼 수 있는데, 이를 dangling comma라 부릅니다. another_circle을 정의하는 줄을 보면 아시다시피, dangling comma가 없어도 코드는 정상적으로 동작합니다.

console.log(JSON.stringify(circle));  // {"x":1.2,"y":3.4}
console.log(JSON.parse('{"x":1.2, "y":3.4}'));  // { x: 1.2, y: 3.4 }

JSON(JavaScript Object Notation)은 JavaScript의 Object를 표현하는 데 널리 쓰이는 형식으로, 문자열 형태를 가집니다. JSON.stringify()로 Object를 문자열화시킬 수 있고, 문자열을 JSON.parse()로 Object화 시킬수도 있습니다.

Array

Python의 List와 유사합니다. 배열이지만 가변적으로 크기를 조절할 수 있으며, 원소의 type이 각각 달라도 괜찮고, 원소에 또다른 List나 Object를 넣을 수도 있습니다.

let squares = [ 0, 1, 4, 9 ];
console.log(squares[2]);  // 4

console.log(squares[6]);  // undefined (No Error)
squares[5] = 5*5;  // (No Error)

console.log(squares.length); // 6
console.log(squares);  // [ 0, 1, 4, 9, <1 empty item>, 25 ]

// Python과는 다르게, X in Y 연산자는 "Y라는 Array에 X라는 인덱스가 있는지" 확인합니다.
console.log(16 in squares);  // false, square[16]은 정의되지 않았으므로.
console.log(5 in squares);  // true, sqaures[5]은 정의되었으므로


let arr = new Array(3).fill("plate");
console.log(arr);  // [ "plate", "plate", "plate" ]
arr.push("my plate");
console.log(arr);  // [ "plate", "plate", "plate", "my plate" ]
console.log(arr.pop());  // "my plate"
console.log(arr);  // [ "plate", "plate", "plate" ]

예제 코드에 명시된 push(), pop() 이외에도 수많은 유용한 Method들이 정의되어 있습니다.

제어문

대부분의 문법이 C와 비슷합니다.

조건문

Short circuit logical operator ||를 이용하면 다음과 같이 깔끔한 문법으로 기본값을 정의할 수 있습니다. 이를테면 다음 코드의 xy는 동일한 동작을 합니다.

const x = user_input ? user_input : "DEFAULT";
const y = user_input || "DEFAULT";

반복문

const o = { one: 1, two: 2, three: 3 }
for (let k in o) {
  console.log(`${k}: ${o[k]}`);
}

// Expected Result:
// one: 1
// two: 2
// three: 3
const a = [ 1, 2, 3 ];
let sum = 0;

for (let v of a) {
  sum += v;
}

console.log(sum);  // 6

예외 처리

try {
  [오류가 발생할 것만 같은 코드;]
  throw [예외 변수];  // 주로 Error Object를 던진다.
} catch([예외 변수]) {
  [예외를 처리하는 코드;]
} finally {
  [try나 catch문이 끝난 뒤 실행되는 블록 (생략 가능);]
}

함수

function function_name(x, y) {
  return x + y;
}

const sum = function_name;
console.log(sum(7, 5));  // 12

const o = {};
o.func = sum;
console.log(o.func(7, 5)); // 12

/* Anonymous Function */
o.subtract = function(x, y) {
  return x - y;
}
console.log(o.subtract(7, 5));  // 2

함수 이름 뒤에 ([매개변수 목록])을 붙여 호출 할 수 있다는 점만 제외하면 Object 같은 참조형 Data Type과 사실상 똑같이 다루어집니다.

const dog = {
  age: 2,
  bark() { return 'Woof!'; },
  sleep() { return 'ZZZ'; },
};

console.log(dog.bark()); // Woof!

위와 같이 Object 속성으로 함수를 직접 정의할 수도 있습니다. 이는 일종의 단축 명령어로써 bark: function() { return 'Woof!'; }, 와 동일한 역할을 수행합니다.

this

통상적으로, 함수 안에 쓰인 this는 해당 함수를 속성으로 갖고 있는 Object를 참조합니다. 다만 함수가 호출되는 맥락에 따라서 의도하지 않은 동작을 할 수도 있습니다.

const remocon = {
  channel: 1,
  up() { this.channel++; },
  down() { this.channel--; },
}


remocon.up();
remocon.up();
console.log(remocon.channel);  // 3


let remocon_up = remocon.up;
remocon_up();
console.log(remocon.channel);  // 3. Why?


remocon_up = remocon.up.bind(remocon);
remocon_up();
console.log(remocon.channel);  // Finally, 4.

[함수].bind() 또는 [함수].call() 을 이용하여 this를 명시적으로 지정해줄 수 있습니다.

this의 구체적인 동작은 난해하기 때문에 지금은 잘 이해가 가지 않아도 괜찮습니다. JavaScript에서는 함수가 특정한 위치에 묶이지 않고 자유롭게 이동이 가능하기 때문에 위와 같은 문제가 생긴다 정도만 알아두세요.

Arrow Notation

let sum1 = function(x, y) { return x + y; }
let sum2 = (x, y) => x + y;
console.log(sum1(3, 4) === sum2(3, 4));  // true

function이라는 키워드 대신 =>을 사용하여 함수를 정의할 수도 있습니다.

([매개변수1], [매개변수2], ...) => {
  [구문1;]
  [구문2;]
  ...
  return [표현식];
}

Array#map(), Array#filter()

let degrees = [ 15, 30, 60, 90, 180, 270 ];
console.log(degrees.map(x => x * Math.PI / 180));
console.log(degrees.filter(x => x >= 90));

// Expected Output:
// [ 0.2617993877991494,
//  0.5235987755982988,
//  1.0471975511965976,
//  1.5707963267948966,
//  3.141592653589793,
//  4.71238898038469 ]
// [ 90, 180, 270 ]

Array#map()과 같이 함수를 매개변수로 넘겨줘야하는 메서드들도 많이 있으며, 이를 잘 활용하면 Array를 매우 효율적으로 다룰 수 있습니다.

스코프

let x = "Public";

{
  let y = "So Secret";
  console.log(x);  // Public
}

console.log(y);  // ReferenceError

블록 { } 안에서 let으로 변수를 선언하면, 블록 바깥에서는 그 변수에 접근할 수 없습니다. (위 예제의 y) 변수가 접근 가능한 “영역”을 스코프 라고 부릅니다.

let get_secret;

{
  let y = "So Secret";
  get_secret = () => y;
}

console.log(get_secret());  // So Secret

get_secret() 함수는 y에 접근할 수 있는 스코프에서 정의되었습니다. 이 함수는 전역 스코프에 대입(assign)되고 y 스코프가 끝난 뒤 호출합니다.

눈여겨볼 점은 y라는 변수가 스코프가 끝났음에도 여전히 존재한다는 점입니다. 따라서 get_secret()을 이용해 간접적으로 스코프 바깥에서 스코프 안의 함수에 접근할 수 있습니다. 이러한 현상을 “클로저”라고 부릅니다.

let make_vault = function(secret, password) {
  return {
    get_secret(user_input) {
      return user_input === password ? secret : null;
    }
  }
}

let vault = make_vault(17, 1234);
console.log(vault.get_secret(4321));  // null
console.log(vault.get_secret(1234));  // 17

함수 또한 독자적인 스코프를 구축합니다. 위 예제에서는 클로저를 이용해 함수 스코프 바깥에서 secret이랑 password에 접근하는 예제를 보여주고 있습니다.

Immediately Invoked Function Expression

x = 3;
(function() {
  var x;
  console.log(x);  // undefined
})();

가끔 JS 코드를 보다 보면 익명 함수를 만들고 이를 ( )로 감싸서 즉시 호출(())하는 패턴을 발견할 수 있습니다. 이를 IIFE라고 부릅니다. 굳이 이런 표현을 쓰는 이유는 독자적인 스코프를 구축하기 위합니다. 만약 위의 코드가 IIFE 패턴을 따르지 않았다면 아래의 코드가 만들어졌을 텐데, 이때는 스코프 바깥에 있는 x = 3이라는 구문이 스코프 안의 var x에 간섭해서 예상치 못한 결과가 나타날 수 있습니다.

물론 이 단순한 예제의 경우 var x가 아닌 let x를 사용함으로써 해결할 수 있습니다.

x = 3;
{
  var x;
  console.log(x);  // 3
}

객체 지향

일관된 구조의 Object를 생성하고 싶은 경우, class를 선언하여 해결할 수 있습니다.

class Account {
  constructor(name) {
    this.name = name;
    this.money = 0;
  }

  deposit(amount) {
    (amount > 0) && (this.money += amount);
  }

  withdraw(amount) {
    return this.money > amount ? (this.money -= amount, amount) : 0;
  }
}

let my = new Account('JM L.');
console.log(my);  // Account { name: 'JM L.', money: 0 }
console.log(my.money);  // 0
my.deposit(5000);
my.deposit(-2000);
console.log(my.money);  // 5000
console.log(my.withdraw(2000));  // 2000
console.log(my.withdraw(4000));  // 0

클래스는 new 연산자를 이용하여 구현할 수 있습니다. 구현체를 인스턴스(Instance) 라고 부릅니다. 클래스 안에 정의한 함수는 메서드 라고 부르며, 메서드 내에서 this는 인스턴스 Object를 의미합니다. constructor() 함수는 생성자 라고 부르며, new 연산자에 의해 함수가 구현될 때 제일 처음으로 호출되는 함수입니다.

class ImprovedAccount extends Account {
  static get_id() {
    ImprovedAccount.id = (ImprovedAccount.id + 1) || 1;
    return ImprovedAccount.id;
  }

  constructor(name) {
    super(name);
    this.id = ImprovedAccount.get_id();
  }

  withdraw(amount) {
    if (amount < 0) throw new Error("withdraw(): amount should be nonnegative!!");
    return super.withdraw(amount);
  }

  toString() {
    return `[${this.id}] ${this.name}'s Account: ${this.money} Won`
  }
}

console.log(new ImprovedAccount('DH K.').toString());

let jsp = new ImprovedAccount('JS P.');
console.log(jsp.id);  // 2
console.log(my instanceof ImprovedAccount);  // false
console.log(jsp instanceof ImprovedAccount);  // true
console.log(jsp instanceof Account);  // true
jsp.deposit(10000);
console.log(jsp.withdraw(4000));  // 4000
jsp.withdraw(-100);

기존 클래스에 있는 속성과 메서드를 보존한 채로 확장된 기능을 구현하고 싶은경우, 클래스를 상속 할 수 있습니다. 이때 상속당한 클래스를 Superclass, 상속한 클래스는 그 Superclass의 Subclass라고 부릅니다. 클래스를 상속한 뒤 함수를 재정의할 수 있는데, 이를 함수 오버라이딩(Overriding) 이라고 부릅니다. 함수를 오버라이딩 하는 경우 super 키워드로 Superclass를 참조할 수 있습니다.

앞에 static 키워드를 붙여진 메서드를 클래스 메서드 라고 부릅니다. 일반 메서드와 달리, 클래스의 모든 구현체는 같은 클래스 메서드를 참조합니다. 클래스 메서드 안에서 this는 인스턴스가 아닌 클래스 변수를 가리킵니다. 그러나 그보다는 클래스 이름을 이용해 클래스 변수에 접근하는 것이 혼란을 막기 좋습니다.

[인스턴스] instanceof [클래스][인스턴스]가 클래스의 구현체거나 클래스의 Subclass의 구현체일 때 참입니다.

웹 브라우저 제어하기

사실 지금까지 설명한 JS의 적지 않은 부분(for ... of ..., class, Arrow Function)은 ES6에서 새로 개발된 기능입니다. React로 개발하기 위해서는 ES6 문법에 반드시 익숙해질 필요가 있지만, 웹 페이지에서 JS를 쓸 때는 잊어선 안 될 한 가지 사항이 있습니다.

IE11의 ES6 지원률은 11%...

만약 “그 브라우저”에서도 문제없이 돌아가는 스크립트를 작성하려면, ES5 표준을 지켜 개발하는 것이 바람직할 것입니다. 또는 ES6 코드를 작성한 다음 Babel과 같은 트랜스컴파일러를 이용해 ES5 코드로 변환하는 방법도 있습니다. 본 세미나에서는 Babel로 트랜스컴파일 하는 과정까지는 다루지 않을 것이지만, 실제로 JS를 작업하실 때는 이를 반드시 염두에 두시길 바랍니다.

DOM

<!DOCTYPE html>
<html lang="en" dir="ltr">
  <head>
    <meta charset="utf-8">
    <title>JS Seminar</title>
  </head>
  <body>
    <input type="button" id="btnAdd" value="Add">
    <input type="button" id="btnRemove" value="Remove">
    <div id="content">
    </div>
    <script type="text/javascript">
      let counter = 0;
      let content = document.getElementById('content');

      document.getElementById('btnAdd')
        .addEventListener('click', function() {
          counter++;
          content.innerHTML += `<p>${'*'.repeat(counter)}</p>`;
        });

      document.getElementById('btnRemove')
        .addEventListener('click', function() {
          if (counter > 0) {
            counter--;
            content.removeChild(content.lastElementChild);
          } else {
            alert('There is nothing to remove!');
          }
        });
    </script>
  </body>
</html>

document는 현재 HTML 파일의 DOM(Document Object Model) 구조를 보여주는 Object를 반환합니다. HTML 수업에서 간략히 설명했던 대로, HTML 문서를 구성하는 HTML 태그 사이의 관계는 일종의 트리로 표현할 수 있는데, document의 메서드를 호출함으로써 해당 트리에서 관심있는 Node를 가져오고, 그것의 HTML 속성을 편집할 수 있습니다.

기타 도움될 만한 개념

References