이 시리즈는 You Don't Know JS Yet (1판인 You Don't Know JS의 개정판) 을 가지고 스터디를 진행하면서 요약 정리한 글입니다. 내용에 대한 퀴즈도 깃헙 리포지토리에 올리고 있으니 참고하시면 더 도움이 되실 것 같습니다. 내용에 대해 오류가 있거나 궁금하신 점이 있다면 댓글 달아주세요!
이번 챕터는 JS를 크게 곁핥는 내용이라 다루는 내용이 많을 수 있습니다. 천천히 여러번 읽어보면서 큰 틀을 잡으면 좋을 것 같습니다!
JS에서는 각각의 파일이 별도의 프로그램으로 작동됩니다. 그렇기 때문에 여러 JS 파일으로 이루어진 서비스가 있을 때 한 파일 안에서 오류가 발생하면 전 서비스가 터지지 않고 그 파일에 관련된 기능등만 작동되지 않는 상황에서 서비스는 부분적으로 작동됩니다. 다만 Babel과 같이 여러 파일들을 한 파일로 묶는 작업을 진행하게 된다면 (당연히) 서비스 전체가 작동되지 않을 수 있습니다.
만약 여러 파일들을 한 프로그램과 같이 작동되게 만드려면 각 파일들의 상태(state)를 공유하는, "global scope"에서 구현하게 된다면 만들 수 있을 것입니다. 대표적으로 모듈(module)로 구현하게 된다면 위와 같은 상태를 만들 수 있을 것입니다. 모듈을 사용할지 아님 각각의 파일로 작동되게 할 지는 서비스 상황에 따라 선택하면 됩니다.
JS의 가장 기저에 있는 개념은 값입니다. JS에서의 값은 원시값(primitive)과 객체(object)가 있습니다. primitive에는 문자열, 숫자, 부울(boolean), bigint, undefined, Symbol 총 여섯개가 존재하고, primitive가 아닌 다른 모든 값 (객체, 클래스 등)은 객체입니다.
문자열은 '
을 사용하거나 "
를 사용해도 상관이 없습니다. 다만 시작과 끝을 서로 맞춰줘야 이 값이 문자열이라고 인식할 수 있습니다. 그러므로 둘 중 하나를 쓴다고 정하면, 두개의 따음표를 혼용해서 쓰지 않고 그 따음표를 계속 사용하는 것을 추천합니다.
firstName = "Brian";
console.log(` My name is ${ firstName }`);
// My name is Brian
또한 `
(Backtick)를 사용해 문자열을 만들 수 있는데, 이때 특이한 기법을 사용할 수 있습니다. 위 코드와 같이 Backtick 안에 ${ ... }
을 사용해 변수의 값을 넣을 수 있는데, 이를 interpolation이라고 합니다.
사실 undefined를 말할 때 null과 같이 말하지만, null은 엄격히 말했을 때 Primitive value가 아닙니다. 책에서도 값의 공백을 저장할 때, null보단 undefined를 사용하는 것이 깔끔하다고 적혀있습니다.
값을 구분하는 쉬운 방법은 typeof
연산자를 사용하는 것입니다. Primitive 값의 경우 해당 값의 타입을 알려주고, 아닌 경우 object
를 반환합니다. 추가적으로 함수의 경우에는 object
가 아니라 function
을 반환합니다.
JS에서 변수를 사용할 때엔 3가지 키워드, var
, let
, const
를 사용합니다. 가장 쉬운 const
의 경우, 스코프 안에서 사용 가능하며 한번 할당된 값은 재할당이 불가능합니다.
const isChange = false;
isChange = true; // Error!
const people = [
"Sungwoo Park", "Seongwoo Park"
];
people[2] = "Woosung Park"; // OK ?
people = []; // Error!
다만 위와 같이 Object value의 경우 const로 변수를 설정해도 안 값은 바뀔수 있는데, 이는 값과 참조의 차이 때문에 저렇게 작동됩니다. 이에 대해선 이후 내용에서 다룰 예정입니다.
var adult = true;
if (adult)
{
var myName = "Sungwoo";
let age = 22;
console.log("Secret info!");
}
console.log(myName);
// Sungwoo
console.log(age);
// Error!
var
와 let
은 기능 자체는 둘다 같지만, 적용 범위에서 차이가 있습니다. 우리가 (다른 언어들에서) 흔히 아는 "block scoping"을 적용 범위로 가지는 변수는 let
이고, var
는 JS만의 특별한 문법 hoisting을 통해 적용 범위를 let
보다 더욱 느슨하게 사용할 수 있습니다. 위 코드에서는 myname
과 age
모두 다 if 블록에서 선언했지만, var
는 더욱 넓게 사용할 수 있기 때문에 myName
을 if 블록 밖에서도 사용할 수 있고 let
은 블록 범위 밖에서 사용할 수 없기 때문에 age
를 출력할 때 에러가 나게 됩니다.
저는 평소에 var
를 거의 사용하지 않고 let
과 const
만을 사용하지만, 책에서는 var
를 사용해 할 수 있는 기능들도 있기 때문에 익숙히 해 둬야 한다고 말하고 있습니다.
JS에서는 Function이 존재하지만 이를 더 확장해 Procedure로 보아야 합니다. 함수와 다르게 Procedure는 하나의 반환값이 아닌 둘 이상의 여러 값을 반환할 수 있습니다.
// 함수선언식
function awesomeFunction(myThings) {
// ..
return amazingStuff;
}
// 함수표현식
var coolFunction = function(myThings) {
// ..
return amazingStuff;
}
JS에서 Function을 만드는 방법은 여러가지가 있는데 대표적으로 함수선언식과 함수표현식이 있습니다. 별 차이는 없어보이지만 이 값들을 할당받을 떄 시간이 서로 다릅니다. 함수선언식의 경우 실행 이전에 컴파일 과정에서 awesomeFunction
과 값들을 받지만, 함수표현식의 경우 컴파일 이후 실행 과정에서 값을 받게 됩니다.
var whatToSay = {
greeting() {
console.log("Hello!");
},
question() {
console.log("What's your name?");
},
answer() {
console.log("My name is Kyle.");
}
};
whatToSay.greeting();
// Hello!
아까 위에서 언급했듯, JS에서 Function은 Procedure라고 할 수 있습니다. 여러 값을 반환하고 싶을땐, 값을 객체 형식으로 바꾸어 위와 같이 반환할 수 있습니다.
값을 비교하는 것은 프로그램에서 중요한 역활을 합니다. 값을 비교하며 프로그램의 분기를 설정할 수도 있고, 이 기능이 작동할지 안할지도 설정할 수 있기 때문입니다. 특히나 JS는 비교와 관련해 엄청난 호불호를 가지고 있는데, 여러 짤도 돌아다니고 있습니다. 이를 이해하기 위해 쉽게 정리해봅시다.
42 == "42"; // true
1 == true; // true
"10" > "9"; // FALSE!!
JS에서 ==
연산자는 equality 보단 equivalence의 역활을 하고 있다고 보면 쉽게 이해할 수 있습니다. ==
는 타입이 같지 않더라도, Coercive Comparison이란 기법을 통해 값을 한 타입으로 변화하여 비교합니다. 가장 먼저, 타입이 같은 값들은 일반 비교하는 것 처럼 비교합니다. 만약 타입이 다르고 비교하는 값 중에 숫자가 있다면, 숫자가 아닌 값을 숫자로 변환해 이를 비교합니다. 위 코드에서 첫번째와 두번째 줄의 경우 "42"
는 42
로 변환되어 같게 처리되고, true
는 1
로 변환되어 같게 처리되는 것입니다. 다만, 타입이 다른데 숫자가 없는 경우에는 문자열로 변환해 알파뱃순으로 비교됩니다. 그러므로 세번째 줄의 비교는 참이 아니라 거짓이 되는 것입니다...😱
42 === "42"; // false
1 === true; // false
var x = [1, 2, 3];
var y = x;
y === x; // true
y === [1, 2, 3]; // false
x === [1, 2, 3]; // false
만약 equality의 의미로 같음을 사용하고 싶다면 ===
연산자를 사용해야 합니다. ==
와 다르게 값과 타입 모두 다 비교해 평소에 알던 같음을 비교할 수 있습니다. 다만 이는 Primitive value에 대해 작동되고, Object는 다르게 작동됩니다. 이도 위에서 언급한 Value와 Reference의 차이 때문에 일어난 결과인데, 이도 추후에 언급할 예정입니다.
JS에서 많이 사용하는 코드 패턴은 Class와 Modules입니다. Class의 경우 다른 객체지향 언어들과 같이 생성자, 상속 등을 사용합니다. (이에 대한 설명은 생략하겠습니다)
function Publication(title,author,pubDate) {
var publicAPI = {
print() {
console.log(`
Title: ${ title }
By: ${ author }
${ pubDate }
`);
}
};
return publicAPI;
}
function Book(bookDetails) {
var pub = Publication(
bookDetails.title,
bookDetails.author,
bookDetails.publishedOn
);
var publicAPI = {
print() {
pub.print();
console.log(`
Publisher: ${ bookDetails.publisher }
ISBN: ${ bookDetails.ISBN }
`);
}
};
return publicAPI;
}
var YDKJS = Book({
title: "You Don't Know JS",
author: "Kyle Simpson",
publishedOn: "June 2014",
publisher: "O'Reilly",
ISBN: "123456-789"
});
YDKJS.print();
// Title: You Don't Know JS
// By: Kyle Simpson
// June 2014
// Publisher: O'Reilly
// ISBN: 123456-789
Module의 경우 클래스와 엄청 비슷한데, 가장 큰 차이점은 객체를 함수를 통해 만든다는 것입니다. 클래스와 다르게 new
연산자를 사용하지 않고 함수를 호출해 생성할수 있습니다. 또한, 클래스에선 this
를 사용해 값을 저장하지만 모듈에서는 global scope에서 값을 저장해도 객체 안에서 값을 사용할 수 있습니다.
// public.js
function printDetails(title,author,pubDate) {
console.log(`
Title: ${ title }
By: ${ author }
${ pubDate }
`);
}
export function create(title,author,pubDate) {
var publicAPI = {
print() {
printDetails(title,author,pubDate);
}
};
return publicAPI;
}
// print.js
import { create as newPublic } from "public.js";
var forAgainstLet = newPublic(
"For and against let",
"Kyle Simpson",
"October 27, 2014"
);
forAgainstLet.print();
// Title: For and against let
// By: Kyle Simpson
// October 27, 2014
위에서 소개한 모듈은 classic module이고, ES6부턴 ES module이 추가되었습니다. ES modules는 클래식 모듈과는 다르게 파일 기준으로 모듈을 나눕니다. 다시말해, 한 파일에 한 모듈을 넣을 수 있습니다. 그렇기 때문에 클래식 모듈처럼 함수를 감싸지 않아도 파일 단위로 모듈을 만들기 때문에 직접 작성만 하면 됩니다. 실제 사용할 때엔 import
를 통해 집어넣을 수 있습니다. 집어넣고 싶은 함수들은 모듈에서 export
키워드를 통해 내보내야 됩니다. import
를 하면 싱글톤이기 때문에 생성할 때엔 같은 싱글 인스턴스를 사용하게 됩니다.