0%

Introducing JSX

1
const element = <h1>Hello, world!</h1>;

위에 예시에 테그는 문자열도 아니고 html도 아니다. 이것은 JSX라고 불린다. React는 JSX로 UI를 표현하기를 추천한다. JSX는 템플릿을 상기시키지만 자바스크립트에 모든 기능이 포함되어 있다.

JSX는 React에 엘리먼트를 생성한다. 우리는 이것이 DOM으로 렌더링 되는것을 다음 섹션에서 살펴볼 것이다. JSX에 기본적인 내용은 아래와 같다.

Why JSX?

React는 렌더링 로직이 본질적으로 다른 UI로직과 결합된다는 것을 받아들인다. (어떻게 이벤트가 다뤄지지, 어떻게 시간이 흐름에 따라 상태가 변하지, 디스플레이를 위해 데이터가 어떻게 준비되지…)

인위적으로 기술을 마크업 과 로직을 분리한 파일로 넣음으로서 분리하는 대신에 리액트는 관심사를 컴포넌트라고 불리는 로직과 마크업을 포함한 느슨한 단위 결합으로 나눈다.

리액트가 JSX를 필수적으로 필요로 하는것은 아니지만, 자바스크립트 코드로 UI를 작업할때 시각적으로 많은 도움이 된다는것을 많은 사람들이 발견했다. 또한 이는 리액트가 유용한 에러와 경고 메세지를 표시할수 있도록 해줍니다.

Embedding Expression in JSX

변수를 램핑해서 사용할 수 있다.

1
2
3
4
5
function App() {
const name = "koo";
const element = <h1>Hello {name}</h1>;
return element;
}

어떤 자바스크립트에 표현식도 이 안에 서용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function userNameElemnt(user) {
return (
<h1>
내 이름은 {user.name} 이고 {user.age}살이야.
</h1>
);
}

const user = {
name: "koo JaYoon",
age: 27,
};

function App() {
const element = <header>{userNameElemnt(user)}</header>;
return element;
}

함수를 넣는것도 가능하다. 필수는 아니지만 가독성을 위해 줄을 나눌 것과 자동 세미콜론 방지를 위해서 괄호로 래핑할 것을 추천한다.

JSX is an Expression Too

편집이 끝나면, JSX 표현식이 자바스크립트 정규 함수로 호출되고 자바스크립트에 객채로 평가 된다.

이것은 JSX를 if 나 for 에 사용해서 인수를 넣거나 할당하거나 리턴할 수 있단느 것을 의미한다.

1
2
3
4
5
6
7
8
9
10
11
function getStranger(user) {
if (user) {
return <h1>{userNameElemnt(user)}</h1>;
}
return <h1>Hello Stranger</h1>;
}

function App() {
const element = <header>{getStranger()}</header>;
return element;
}

Specifying Attributes with JSX

스트링 문자열을 quotes를 사용해 속성으로 정의할 수 있다. 또한 중괄호를 사용해서 자바스크립트 표현식을 속성으로 사용할 수 도 있다.

1
2
3
const element = <div tabIndex="0"></div>;

const cuElemnt = <img src={user.avatarUrl}></img>;

중괄호를 사용할때 qutos를 사용하지 않도록 주의해라.

JSX는 HTML 보다는 JavaScript에 가깝기 때문에 camelCase를 프로퍼티로 사용한다.

Specifying Children with JSX

빈태그일 경우 XML 과 비슷하게 />를 사용해서 닫아준다.

1
const element = <img src={user.avatarUrl} />;

JSX 태그는 자식 태그를 포함할 수 있다.

1
2
3
4
5
6
const element = (
<div>
<h1>Hello!</h1>
<h2>Good to see you here.</h2>
</div>
);

JSX Prevents Injection Attacks

유저에 입력을 JSX에 삽입시키는 것은 안전하다.

1
2
3
const title = response.potentiallMaliciousInput;
// This is safe:
const element = <h1>{title}</h1>;

기본적으로 React DOM 은 렌더링 하기전에 JSX 내장된 어떤 값이든 이스케이프 한다. 그러므로 애플리 케이션에서 명시적으로 적히지 않은 내용은 주입되지 않는다. 모든 것은 렌더링 하기전에 문자열로 변환된다. 이것은 XSS 공격을 막는데 도움을 준다.

JSX Represents Objects

Babel은 JSX를 React.createElement()에 호출로 컴파일한다.

아래 두개는 동일하다.

1
const element = <h1 className="greeting">Hello, world!</h1>;
1
2
3
4
5
const element = React.createElement(
"h1",
{ className: "greeting" },
"Hello, world!"
);

`React.createElement()’ 너가 버그 없는 코드를 작성하하도록 도움이되는 몇가지 체크를 수행한다. 하지만 이것은 본질적으로 다음에 객체를 생상한다.

1
2
3
4
5
6
7
8
// Note: this structure is simplifed
const element = {
thpe: "h1",
props: {
className: "greeting",
children: "Hellok world!",
},
};

이 객체는 React elements라고 불린다. 이것을 너가 스크린에서 보고싶은 것에 설명이라고 생각할 수 있다.
React는 이 객체를 DOM 을 생상하고 최신상태로 유지하는데 사용한다.

다음 섹션에서 React elements가 DOM으로 렌더링 되는것을 살펴볼 것이다.

당신의 에디터에 Babel 언어 설정을 선택하기를 추천한다. 그러면 ES6 와 JSX 코드가 하이라이트 될것이다.

async와 await

async함수

사용 법

1
2
3
async function f() {
return 1;
}

async 를 붙히면 프로미스를 반환되게 할 수 있다. 함수가 프로미스가 아닌 값을 반환하더라도 이행 상태의 프라미스로 감싼 것으로 반환한다.

1
2
3
4
5
async function f() {
return 1;
}

f().then(alert); // 1

명시적으로 promise를 반환되게 하는것도 가능하디.

1
2
3
4
5
async function f() {
return Promise.resolve(1);
}

f().then(alert); // 1

await

awaitasync 함수 안에서만 사용할 수 있다. 사용법은 다음과 같다.

1
2
// await는 async 함수 안에서만 동작합니다.
let value = await promise;

await는 키워드는 프라미스가 처리될때까지 기다린다.

1
2
3
4
5
6
7
8
9
10
11
12
async function f() {

let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("완료!"), 1000)
});

let result = await promise; // 프라미스가 이행될 때까지 기다림 (*)

alert(result); // "완료!"
}

f();

await는 프라미스가 처리될때까지 함수 실행을 기다리게 한다. 프라미스가 처리 되길 기다리는동안 다른 일(다른 스크립트 실행, 이벤트 처리) 등을 할 수 있어서 CPU 리소스가 낭비되지 않는다.

promise.then 보다 가동성이 좋고 쓰기 좋다.

사용 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
async function showAvatar() {

// JSON 읽기
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();

// github 사용자 정보 읽기
let githubResponse = await fetch(`https://api.github.com/users/${user.name}`);
let githubUser = await githubResponse.json();

// 아바타 보여주기
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);

// 3초 대기
await new Promise((resolve, reject) => setTimeout(resolve, 3000));

img.remove();

return githubUser;
}

showAvatar();
  • await는 thenable 객체를 받는다. .then이 구현되어 있으면 받을 수 있다.
  • async 클래스 메서드 . 클래스 메서드에 async를 추가하면 프라미스를 반환하는 메서드를 만들 수 있다. 동일하게 이런 메서드 안에서는 await를 사용할 수 있다.

에러핸들링

await promise에서 만약 에러가 발생하면 에러를 던진 것과 동일하게 동작한다.

1
2
3
async function f() {
await Promise.reject(new Error("에러 발생!"));
}

위에 코드는 아래 코드와 동일하다.

1
2
3
async function f() {
throw new Error("에러 발생!");
}

await가 던진 에러는 try..catch문을 이용하여 잡을 수 있다.

1
2
3
4
5
6
7
8
9
10
async function f() {

try {
let response = await fetch('http://유효하지-않은-주소');
} catch(err) {
alert(err); // TypeError: failed to fetch
}
}

f();

try..catch 가 없으면 함수 결과로 거부된 프라미스과 반환될테니 .catch()를 통해 에러를 헨들링할 수 있다.

1
2
3
4
5
6
async function f() {
let response = await fetch('http://유효하지-않은-url');
}

// f()는 거부 상태의 프라미스가 됩니다.
f().catch(alert); // TypeError: failed to fetch // (*)

async 함수 안에서 await 키워드를 붙여서 Promise.all를 반환 받을 수 있다.

1
2
3
4
5
6
// 프라미스 처리 결과가 담긴 배열을 기다립니다.
let results = await Promise.all([
fetch(url1),
fetch(url2),
...
]);

제너레이터

제너레이터를 사용하면 여러 개의 값을 필요에 따라 하나씩 반환 할 수 있다.

제너레이터 함수

사용법

1
2
3
4
5
function* generateSequence() {
yield 1;
yield 2;
return 3;
}

제너레이터 함수는 코드가 실행되지 않고 대신에 실행을 처리하는 특별 객체, ‘제너레이터 객체’ 가 반환된다.

1
2
3
4
5
6
7
8
9
function* generateSequence() {
yield 1;
yield 2;
return 3;
}

// '제너레이터 함수'는 '제너레이터 객체'를 생성합니다.
let generator = generateSequence();
alert(generator); // [object Generator]

next()는 제너레이터의 주요 메서드이다. 이 메서드를 호출하면 yield <value>를 만날때까지 계속 실행되다가 value를 반환한다. value를 지정 안할 수 도 있는데 이 경우 undefined가 반환된다.

next()는 항상 다음 두가지 프로퍼티를 가진 객체를 반환한다.

  • value : 산출 값
  • done : 함수 코드 실행이 끝났으면 true, 아니라면 false
1
2
3
4
5
6
7
8
9
10
11
function* generateSequence() {
yield 1;
yield 2;
return 3;
}

let generator = generateSequence();

let one = generator.next();

alert(JSON.stringify(one)); // {value: 1, done: false}

제너레이터는 yield 1 부분에 멈추고 아직 제너레이터 가 끝난게 아니기 때문에 done: false 가 된다.

같은 방법으로 2번 더 .next()를 호출하면 마지막에 { value: 3, done: true} 인 객체를 반환하고 제너레이터는 종료 된다. 이 다음 부터는 .next()를 호출하더라도 {done: true}인 객체만 반환할 뿐이다.

제너레이터와 이터러블

제너레이터는 이터르블 이다. 따라서 for..of 반복문을 사용해 값을 얻을 수 있다.

1
2
3
4
5
6
7
8
9
10
11
function* generateSequence() {
yield 1;
yield 2;
return 3;
}

let generator = generateSequence();

for (let value of generator) {
alert(value); // 1, 2가 출력됨
}

그런데 주의할 점은 3은 출력되지 않는다는 것이다. 그 이유는 for..of 이터레이션은 done:true일때 마지막 vlaue를 무시하기 때문이다. 3을 출력되게 하고 싶다면 return이 아니라 yield 3 으로 반환해야 한다.

제너레이터는 이터러블 객체이므로 제너레이터에도 전개 문법 같은 관련 기능을 사용할 수 있다.

1
2
3
4
5
6
7
8
9
function* generateSequence() {
yield 1;
yield 2;
yield 3;
}

let sequence = [0, ...generateSequence()];

alert(sequence); // 0, 1, 2, 3

이터러블 대신 제너레이터 사용하기

이터러블 코드 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
let range = {
from: 1,
to: 5,

// for..of 최초 호출 시, Symbol.iterator가 호출됩니다.
[Symbol.iterator]() {
// Symbol.iterator는 이터레이터 객체를 반환합니다.
// for..of는 반환된 이터레이터 객체만을 대상으로 동작하는데, 이때 다음 값도 정해집니다.
return {
current: this.from,
last: this.to,

// for..of 반복문에 의해 각 이터레이션마다 next()가 호출됩니다.
next() {
// next()는 객체 형태의 값, {done:.., value :...}을 반환해야 합니다.
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
},
};
},
};

// 객체 range를 대상으로 하는 이터레이션은 range.from과 range.to 사이의 숫자를 출력합니다.
alert([...range]); // 1,2,3,4,5

Symbol.iterator 대신 제너레이터 함수를 사용하면, 제너레이터 함수로 반복이 가능하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
let range = {
from: 1,
to: 5,

*[Symbol.iterator]() {
// [Symbol.iterator]: function*()를 짧게 줄임
for (let value = this.from; value <= this.to; value++) {
yield value;
}
},
};

alert([...range]); // 1, 2, 3, 4, 5

원래 제너레이터는 이터레이터를 어떻게 하면 쉽게 구현할지 염두에 두며 자바스크립트에 출시되었다. 제너레이터를 사용하면 이터레이터를 쓰면 복잡한 코드를 같은 기능으로 간결하게 작성할 수 있다.

제너레이터 컴포지션

제너레이터 안에 제너레이터를 임베딩(embedding, composing) 할 수 있게 해주는 제너레이터의 특별 기능이다.

연속된 숫자를 생성하는 제너레이터 함수를 만들어 본다.

1
2
3
function* generateSequence(start, end) {
for (let i = start; i <= end; i++) yield i;
}

문자나 숫자에 적용해서 비밀번호를 생성하는 함수를 반들수 있다. 일반 함수로 구혆가ㅣ 위해서는 여러개의 함수를 만들고 그 호출 결과를 어딘가에 저장한 후 다시 그 결과들을 조합해야 원하는 기능을 구현할 수 있다.
하지만 특수 문법 yield* 를 사용하면 제너레이터를 다른 제너레이터에 끼워 넣을 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function* generateSequence(start, end) {
for (let i = start; i <= end; i++) yield i;
}

function* generatePasswordCodes() {
// 0..9
yield* generateSequence(48, 57);

// A..Z
yield* generateSequence(65, 90);

// a..z
yield* generateSequence(97, 122);
}

let str = "";

for (let code of generatePasswordCodes()) {
str += String.fromCharCode(code);
}

alert(str); // 0..9A..Za..z

yield* 지시자는 실행을 다른 제너레이터에 위임한다. 제너레이터를 대상으로 반복을 수행하고, 산출 값들을 바깥으로 전달한다는 것을 의미한다.

yield 를 사용해 제너레이터 안, 밖으로 정보 교환하기

제너레이터의 yield는 양방향 길과 같은 역할을 한다. yield 는 결과를 바깥으로 전달할 뿐만 아니라 값을 제너레이터 안으로 전달하기까지 한다. 값을 안, 밖으로 전달하려면 generate.next(arg) 를 호출해야 한다.
이때 인수 argyield의 결과가 된다.

1
2
3
4
5
6
7
8
9
10
11
12
function* gen() {
// 질문을 제너레이터 밖 코드에 던지고 답을 기다립니다.
let result = yield "2 + 2 = ?"; // (*)

alert(result);
}

let generator = gen();

let question = generator.next().value; // <-- yield는 value를 반환합니다.

generator.next(4); // --> 결과를 제너레이터 안으로 전달합니다.
  1. generator.next()를 처음 호출할 댄 항상 인수가 없어야 한다. 인수가 들어오면 무시함 이때 yield "2+2=?"의 결과가 반환된다.
  2. 변수 question에 이 값이 할당된다.
  3. generator.next(4) 를 호출하면 인수 4 가 result에 할당된다.

제너레이터와 외부 호출코드는 next/yield를 이용해 결과를 전달 및 교환한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function* gen() {
let ask1 = yield "2 + 2 = ?";

alert(ask1); // 4

let ask2 = yield "3 * 3 = ?";

alert(ask2); // 9
}

let generator = gen();

alert(generator.next().value); // "2 + 2 = ?"

alert(generator.next(4).value); // "3 * 3 = ?"

alert(generator.next(9).done); // true

코드를 실행하면 차례대로 다음과 같이 출력된다.

  • 2 + 2 = ?
  • 4
  • 3 * 3 = ?
  • 9
  • true

generator.throw

generator 외부 코드가 에러를 만들거나 던질 수도 있다. 에러를 yield 안으로 전달하려면 generator.throw(err)를 호출해야 한다. 호출하면 erryield 가 있는 줄로 던져진다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function* gen() {
try {
let result = yield "2 + 2 = ?"; // (1)

alert(
"위에서 에러가 던져졌기 때문에 실행 흐름은 여기까지 다다르지 못합니다."
);
} catch (e) {
alert(e); // 에러 출력
}
}

let generator = gen();

let question = generator.next().value;

generator.throw(new Error("데이터베이스에서 답을 찾지 못했습니다.")); // (2)

(2)에서 던진 에러는 (1) 에서 예외를 만든다. 만약 제너레이터 안에서 에러를 처리 하지 않았다면 외부로 떨어져 나온다. 바깥에서 에러를 처리해도 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
function* generate() {
let result = yield "2 + 2 = ?"; // Error in this line
}

let generator = generate();

let question = generator.next().value;

try {
generator.throw(new Error("데이터베이스에서 답을 찾지 못했습니다."));
} catch (e) {
alert(e); // 에러 출력
}

async 이터레이터와 제너레이터

비동기 이터레이터, 제너레이터 사용하여 비동기적으로 들어오는 데이터를 필요에 따라 처리할 수 있다.

async 이터레이터

일반 이터러블

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
let range = {
from: 1,
to: 5,

// for..of 최초 실행 시, Symbol.iterator가 호출됩니다.
[Symbol.iterator]() {
// Symbol.iterator메서드는 이터레이터 객체를 반환합니다.
// 이후 for..of는 반환된 이터레이터 객체만을 대상으로 동작하는데,
// 다음 값은 next()에서 정해집니다.
return {
current: this.from,
last: this.to,

// for..of 반복문에 의해 각 이터레이션마다 next()가 호출됩니다.
next() {
// (2)
// next()는 객체 형태의 값, {done:.., value :...}를 반환합니다.
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
},
};
},
};

for (let value of range) {
alert(value); // 1, 2, 3, 4, 5
}

이터러블 객체를 비동기적으로 만들기 위한 작업

  1. Symbol.iterator 대신, Symbol.asyncIterator를 사용해야 한다.
  2. next() 는 프라미스를 반환해야 한다.
  3. 비동기 이터러블 객체를 대상으로 하는 반복 작업은 for await (let item of iterable) 반복문을 사용해 처리해야 한다.

1초마다 비동기적으로 값을 반환하는 이터러블 객체를 만들어 본다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
let range = {
from: 1,
to: 5,

// for await..of 최초 실행 시, Symbol.asyncIterator가 호출됩니다.
[Symbol.asyncIterator]() {
// (1)
// Symbol.asyncIterator 메서드는 이터레이터 객체를 반환합니다.
// 이후 for await..of는 반환된 이터레이터 객체만을 대상으로 동작하는데,
// 다음 값은 next()에서 정해집니다.
return {
current: this.from,
last: this.to,

// for await..of 반복문에 의해 각 이터레이션마다 next()가 호출됩니다.
async next() {
// (2)
// next()는 객체 형태의 값, {done:.., value :...}를 반환합니다.
// (객체는 async에 의해 자동으로 프라미스로 감싸집니다.)

// 비동기로 무언가를 하기 위해 await를 사용할 수 있습니다.
await new Promise((resolve) => setTimeout(resolve, 1000)); // (3)

if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
},
};
},
};

(async () => {
for await (let value of range) {
// (4)
alert(value); // 1,2,3,4,5
}
})();

일반 이터레이터와, async 이터레이터에 차이점

  1. 객체를 비동기적으로 반복 가능하도록 하려면, Symbol.asyncIterator메서드가 반드시 구현되어야 함.
  2. Symbol.asyncIterator는 프라미스를 반환하는 next()가 구현된 객체를 반환해야 한다.
  3. next()aync메서드일 필요는 없지만 await를 쓰면 편하기 때문에 편의상 이렇게 사용한다.
  4. 반복 작업을 하려면 for await를 붙인다. 그럼 range[Symbol.asyncIterator] ()가 일회 호출되는데, 그 이후엔 각 값을 대상으로 next()가 호출된다.

전개 문법 ...은 동기적으로 동작하지 않는다. 동기 작업이 필요한 이터레이터는 Symbol.asyncIterator가 아닌 Symbol.iterator을 찾기 때문이다.

async 제너레이터

일반 제너레이터

1
2
3
4
5
6
7
8
9
function* generateSequence(start, end) {
for (let i = start; i <= end; i++) {
yield i;
}
}

for (let value of generateSequence(1, 5)) {
alert(value); // 1, then 2, then 3, then 4, then 5
}

async를 붙여주면 비동기적으로 제너레이터를 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async function* generateSequence(start, end) {
for (let i = start; i <= end; i++) {
// await를 사용할 수 있습니다!
await new Promise((resolve) => setTimeout(resolve, 1000));

yield i;
}
}

(async () => {
let generator = generateSequence(1, 5);
for await (let value of generator) {
alert(value); // 1, 2, 3, 4, 5
}
})();

aync 제너레이의 generator.next()의 메서드는 비동기적이 되고 프라미스를 반환한다. 따라서 await 키워드를 사용해서 generator.next()를 사용하도록 한다.

1
result = await generator.next(); // result = {value: ..., done: true/false}

async 이터러블

반복 가능한 객체를 만들려면 객체에 Symbol.iterator를 추가해야 한다.

1
2
3
4
5
6
7
let range = {
from: 1,
to: 5,
[Symbol.iterator]() {
return <range를 반복가능하게 만드는 next가 구현된 객체>
}
}

일반적으로 제너레이터를 반환하도록 구현하는 경우가 더 많다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let range = {
from: 1,
to: 5,

*[Symbol.iterator]() {
// [Symbol.iterator]: function*()를 짧게 줄임
for (let value = this.from; value <= this.to; value++) {
yield value;
}
},
};

for (let value of range) {
alert(value); // 1, 2, 3, 4, 5
}

지금 이 상태에서 비동기 동작을 축하려면, Symbol.iteratorasync Symbol.asyncIterator로 바꿔야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let range = {
from: 1,
to: 5,

async *[Symbol.asyncIterator]() {
// [Symbol.asyncIterator]: async function*()와 동일
for (let value = this.from; value <= this.to; value++) {
// 값 사이 사이에 약간의 공백을 줌
await new Promise((resolve) => setTimeout(resolve, 1000));

yield value;
}
},
};

(async () => {
for await (let value of range) {
alert(value); // 1, 2, 3, 4, 5
}
})();

실제 사례

모듈

특정 기능별로 클래스나 함수를 묶은 것. 자바스크립트가 커짐에 따라 모듈로 분리할 필요가 생겼다.

모듈이란

스크립트 하나는 모듈 하나이다.

내보내기

1
2
3
4
// 📁 sayHi.js
export function sayHi(user) {
alert(`Hello, ${user}!`);
}

가져오기

1
2
3
4
5
// 📁 main.js
import { sayHi } from "./sayHi.js";

alert(sayHi); // 함수
sayHi("John"); // Hello, John!

모듈의 핵심 기능

엄격 모드로 실행

모듈은 항상 엄격 모드 use strict로 실행된다. 선언되지 않은 변수에 값을 할당하는 등의 코드는 에러를 발생시킨다.

1
<script type="module">a = 5; // 에러</script>

모듈 레벨 스코프

모듈간에는 당연히 변수를 공유하지 않는다.

단 한번만 평가됨

동일한 모듈이 여러 곳에서 사용되더라도 최초 호출 시 단 한번만 실행된다. 실무에서는 최상위 레벨 모듈을 대게 초기화나 내부에서 쓰이는 데이터 구조를 만들고 이를 내보내 재사용하고 싶을 때 사용한다.

최초에 가져온 모듈에서 가한 조작이 다음에 가져오는 상황에서 확인할 수 있다.

1
2
3
4
// 📁 admin.js
export let admin = {
name: "John",
};

모듈 가져오고 조작하기

1
2
3
4
5
6
7
8
9
10
// 📁 1.js
import { admin } from "./admin.js";
admin.name = "Pete";

// 📁 2.js
import { admin } from "./admin.js";
alert(admin.name); // Pete

// 1.js와 2.js 모두 같은 객체를 가져오므로
// 1.js에서 객체에 가한 조작을 2.js에서도 확인할 수 있습니다.

이런 특징을 이용하면 모듈 설정을 쉽게 할 수 있다.

예를 들어 특정 기능을 사용하는 모듈이 사용전에 인증 정보를 받아와야 하는 등의 작업이 필요하다고 생각하면 이 기능을 활용할 수 있다.

1
2
3
4
5
6
// 📁 admin.js
export let admin = {};

export function sayHi() {
alert(`${admin.name}님, 안녕하세요!`);
}
1
2
3
// 📁 init.js
import { admin } from "./admin.js";
admin.name = "보라";
1
2
3
4
5
6
// 📁 other.js
import { admin, sayHi } from "./admin.js";

alert(admin.name); // 보라

sayHi(); // 보라님, 안녕하세요!

import.meta

import.meta 객체는 현재 모듈에 대한 정보를 제공해 주는데 브라우저 환경에서는 스크립트의 url 정보를 얻을 수 있다.

this 는 undefine만

모듈이 아닌 일반 객체에서 this는 전역 객체 window 이지만 모듈 내에서는 undefined이다.

브라우저 특정 기능

브라우저 환경에서 type="module"로 사용할때의 특징. 간단하게만 알아본다.

지연실행

defer 속성이 붙은 것처럼 실행되서 html 을 전부 파싱한 후에 실행된다.

인라인 스크립트의 비동기 처리

async 속성이 붙은 스크립트는 로딩이 끝나면 다른 스크립트나 HTML 문서가 처리되길 기다리지 않고 바로 실행된다.

외부 스크립트

  1. 최최 실행
  2. CORS 헤더가 필요함.

경로가 없는 모듈은 금지

Node.js 같은건 모듈 경로를 알고 있어서 가져올 수 있지만 브라우저는 상대 혹은 절대 경로가 무조건 있어야 함.

빌드 툴

브라우저 환경에서 모듈을 ‘단독’으로 사용하는 경우는 흔치 않다. 대게 웹팩(Webpack)과 같은 특별한 툴을 사용해 모듈을 한데 묶어 프로덕션 서버에 올리는 방식을 사용한다.

일반 스크립트로 변환되기 대문에 type=”module”를 사용할 필요가 없어진다.

모듈 내보내고 가져오기

선언부 앞에 export 붙이기

1
2
3
4
5
6
7
8
9
10
11
12
// 배열 내보내기
export let month = ["Jan", "Feb", "Mar"];

// 상수 내보내기
export const MODULES_BECAME_STANDARD_YEAR = 2015;

// 클래스 내보내기
export class User {
constructor(name) {
this.name = name;
}
}

export classexport function 뒤에는 세미콜론을 붙이지 않는다.

선언부와 떨어진 곳에 export 붙이기

1
2
3
4
5
6
7
8
9
10
// say.js
function sayHi(user) {
alert(`Hello, ${user}!`);
}

function sayBye(user) {
alert(`Bye, ${user}!`);
}

export { sayHi, sayBye };

import *

무언갈 가져오고 싶을 때

1
2
3
4
import { sayHi, sayBye } from "./say.js";

sayHi("John");
sayBye("John");
1
2
3
4
import * as say from "./say.js";

say.sayHi("John");
say.sayBye("John");

편하긴 하지만 구체적으로 무엇을 가져올지 명시적으로 알려주는것이 좋다.

  1. 웹팩과 같은 모던 빌드 툴은 로딩 속도를 높이기 위해 모듈들을 한데 모으는 번들링과 최적화를 수행한다. 이 과정에서 사용하지 않는 리소스가 삭제되기도 한다.
  2. 더 간결하게 적을 수 있다.
  3. 어디에 어떤게 쓰이는지 명확하기 때문에 리팩토링이나 유지보수에 도움이 된다.

import ‘as’

1
2
3
4
import { sayHi as hi, sayBye as bye } from "./say.js";

hi("John");
bye("John");

Export ‘as’

이름 바꿔서 내보낼 수 있다.

1
export { sayHi as hi, sayBye as bye };

export default

export default 라는 특별한 문법을 지원한다. 이는 해당 모듈엔 개체가 하나만 있다는 사실을 명백히 나타낼 수 있다.

1
2
3
4
5
6
// user.js
export default class User {
constructor(name) {
this.name = name;
}
}

보통 파일 하나엔 export default가 하나만 있다.

이렇게 내보내면 import 에서 {}를 제거할 수 있다.

1
2
3
import User from "./user.js"; // {User} 가 아니라 User 이다.

new User("John");

이름으로 가져오는 방식을 named export라고 한다.

파일 하나에 named exportexport default를 함께 사용해도 문제가 되지는 않는다. 하지만 실무에선 파일 하나에 둘중 하나만 사용하는 것이 일반적이다.

또한 export default의 경우에는 중괄호 없이 내보낼 것이 하나밖에 없다고 정한 것이기 때문에 이름없이 내보내는것이 가능하다. 하지만 named export에서는 이름을 반드시 명시해 주어야 한다.

default name

1
2
3
4
5
6
function sayHi(user) {
alert(`Hello, ${user}`);
}

// 함수 선언분에 export default 한 것과 똑같다.
export { sayHi as default };

흔하진 않지만 default를 사용해서 동시에 가져올 수 있다.

1
2
3
4
5
6
7
8
9
10
// user.js
export default class User {
constructor(name) {
this.name = name;
}
}

export function sayHi(user) {
alert(`Hello, ${user}`);
}
1
2
3
4
5
6
7
8
import { default as User, sayHi } from "./user.js";

// 혹은

import * as user from "./user.js";

let User = user.default;
new User("John");

default export 의 이름에 관한 규칙

default export 는 꼭 파일내에서 내보내는 이름으로 불러오지 않아도 되기 때문에 같은 모듈이라도 사용하는 사람에 따라서 불러온 이름이 다를 수 있다. 이는 혼란을 야기할 수 있는데 이것을 방지하기 위해서 이름을 정하는 규칙이 있다.
보통 파일이름에 첫 글자는 대문자인 형식으로 사용한다.

모듈 다시 내보내기

1
2
3
export { sayHi } from "./say.js"; // sayHi를 다시 내보냄

export { default as User } from "./user.js"; // default export 를 다시 내보내기 함.

가져온 객체를 즉시 다시 내보내기 할수 있다. 내보낼 기능을 패키지 전반에 분산하여 구현한 후 필요한 파일에서 가져온후 다시 내보내는 방식으로 활용할 수 있다.

default export 다시 내보내기

1
2
3
4
// user.js
export default class User {
//...
}
  1. Userexport User from './user.js 로 다시 내보내기 할 때 문법 에러가 발생한다. export {default as User}를 사용해야 한다.
  2. export * from './user.js'를 사용했다면 dafault export는 무시 되고 named export 만 다시 내보내진다.

동시에 하기 위해서는 다음과 같이 사용해야 한다.

1
2
export * from "./user.js"; // named export를 다시 내보내기
export { default } from "./user.js"; // default export 다시 내보내기

동적으로 모듈 가져오기

모듈 경로로는 문자열만 가능하기 때문에 함수에 매개변수를 넘겨 줘서 결과값을 갖는 것은 할 수 없다. 또한, 런타임이나 조건부로 모듈을 불러올 수 없다.
그럼에도 불구하고 모듈을 동적으로 불러와야 한다면 어떻게 해야 할까?

import() 표현식

import(module) 표현식은 모듈을 읽고 이 모듈이 내보내는 것들을 모두 포함하는 객체를 담은 이행된 프로미스를 반환한다.

1
2
3
4
5
let modulePath = prompt("어떤 모듈을 불러오고 싶으세요?");

import(modulePath)
.then(obj => //<모듈 객체>)
.catch(err => //<로딩 엘, e.g 해당하는 모듈이 없는 경우>)
1
2
3
4
5
6
7
8
// say.js
export function hi() {
alert("안녕하세요.");
}

export function bye() {
alert("안녕히 가세요.");
}

let module = await import(modulePath) 와 같이 가져올 수 도 있음.

1
2
3
4
let { hi, bye } = await import(modulePath);

hi();
bye();

say.js 에 export default 추가

1
2
3
4
// say.js
export default function () {
alert("export default한 모듈을 불러왔습니다!");
}

export defaul로 내보냈으면 default 프로퍼티를 사용하여 가져올수 있다.

1
2
3
4
let obj = await import("./say.js");
let say = obj.default;

say();

import() 는 함수가 아니다. super()처럼 괄호를 쓰는 특별한 문법이다.

Proxy 와 Reflect

Proxy는 특정 객체의 행동을 가로챔. 직접 처리하기도 하고 넘겨 주기도 하는 방식으로 동작.

Proxy

1
let proxy = new Proxy(target, handler);
  • target 감싸게 될 객체
  • handler 동작을 가로채는 메서드인 트랩이 담긴 객체

타겟을 처리할 트랩이 있으면 트랩이 실행하고 아니면 target 이 직접 수행함.

1
2
3
4
5
6
7
8
9
10
let target = {};
let proxy = new Proxy(target, {}); // 빈 핸들러

proxy.test = 5; // 프락시에 값을 씀.

alert(target.test); // target에 프로퍼티가 생김.

alert(proxy.test); // 프락시를 사용해 값을 읽을수도 있음.

for (let key in proxy) alert(key); // test, 반족도 잘 동작함.

트랩이 없기 때문에 다음과 같이 동작한다.

  1. proxy.test = 하면 target에 값이 설정된다.
  2. proxy.test 를 이용해 값을 읽으면 target 에서 값을 읽어온다.
  3. proxy를 대상으로 반복을 수행하면 target에 지정한 값이 반환된다.

핸들러가 비어있으면 모든 작업은 target에서 수행된다.

객체의 어떤 작업을 할때는 코드로 확인할 수 없는 내부 메서드를 이용하여 수행한다. 프록시를 사용하면 이 내부 메서드를 가로챈다.

new Proxyhandler에 매개변수로 추가할 수 있는 메서드는 아래와 같다.

내부 메서드나 트랩을 쓸 땐 자바스크립트에서 정한 몇 가지 규칙(invariant)를 반드시 따라야 함.

get 트랩으로 프로퍼티 기본값 설정하기

프로퍼티 읽기를 가로채려면 handlerget(target, property, receiver) 메서드가 있어야 한다.

  • target: 동작을 전달할 객체로 new Proxydml 첫번째 인자이다.
  • property: 프로퍼티 이름
  • receiver: 타깃 프로퍼티가 getter 라면 receiver는 getter가 호출될 때 this이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
let numbers = [0, 1, 2];

numbers = new Proxy(numbers, {
get(target, prop) {
if (prop in target) {
return target[prop];
} else {
return 0; // 기본값 undefined 대신에
}
},
});

alert(numbers[1]); // 1
alert(numbers[123]); // 0 해당 요소가 없으므로

프락시로 객체를 감쌌다면 target 객체에 접근할 수 없도록 해야 함. 위에서 number를 덮어씌워 줌으로서 원래의 numbers의 접근하지 못하도록 함.

set 트랩으로 프로퍼티 값 검증하기

set 메서드의 인수는 아래와 같은 역활을 한다.

set(target, property, value, receiver):

  • target: 동작을 전달할 객체로 new Proxy의 첫 번째 인자이다.
  • property: 프로퍼티 이름
  • value: 프로퍼티 값
  • receiver: get 트랩과 유사하게 동작하는 객체로, setter 프로퍼티에만 관여한다.

number 일때만 setter 되는 프록시 만들기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let numbers = [];
numbers = new Proxy(numbers, {
set(target, prop, val) {
if (typeof var == 'number') {
target[prop] = val;
return true;
} else {
return false;
}
}
});

numbers.push(1);
numbers.push(2);
alert(numbers.length); // 2

numbers.push("test") // Error: 'set' on proxy

배열관련 기능들도 여전히 사용할 수 있다.

true를 반환하는 것을 잊지 말아야 한다.

ownKeys와 getOwnPropertyDescriptor로 반복 작업하기

Object.keys, for..in 반복문을 비로한 프로퍼티 순환 관련 메서드 대다수는 내부 메서드 [[OwnPropertyKeys]] (트랩 메서드는 (ownKeys)) 를 사용해 프로퍼티 목록을 얻는다.

_로 시작하는 프로퍼티는 for..in 반복문의 순환 대상에서 제외하도록 만듦.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let user {
name: "John",
age: 30,
_password: "***",
};

user = new Proxy(user, {
ownKeys(target) {
return Object.keys(target).filter(key => !key.startsWith('_'));
}
})

for (let key in user) alert(key); // name, age

alert(Object.keys(user)); // name, age;
alert(Object.values(user)); // John, 30

Object.keysenumerable 플래그가 있는 프로퍼티만 반환한다. ownKeys에서 enumerable 플래그가 없는 객체를 반환했다면, Object.keys를 호출해도 빈 문자열이 반환된다.

이럴땐 getOwnPropertyDescriptor를 사용하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let user = {};

user = new Proxy(user, {
ownKeys(target) { // 프로퍼티 리스트를 받을 때 딱 한번 호출된다.
return ['a', 'b', 'c'];
}

getOwnPropertyDescriptor(target, prop) { // 모든 프로퍼티를 대상으로 호출된다.
return {
enumerable: true,
configurable: true,
// 이 외의 플래그도 반환할 수 있다. "value:..."도 가능하다.
}
}
})

alert(Object.keys(user)); // a, b, c

deleteProperty 와 여러 트랩을 사용해 프로퍼티 보호하기

_가 붙은 프로퍼티는 내부에서만 사용하는 프로퍼티를 의미한다. 외부에서 접근해서는 안되는데 기술적으로 접근 가능한 문제가 있다.

프록시를 이요해서 접근하지 못하도록 막아 본다.

  • get: 프로퍼티를 읽으려고 하면 에러를 던져줌
  • set: 포로퍼티에 쓰려고 하면 에러를 던져줌
  • deleteProperty: 프로퍼티를 지우려고 하면 에러를 던져줌
  • ownKeys: for..in 이나 Object.keys같은 프로퍼티 순환 메서드를 사용할 때 _로 시작하는 메서드는 제외함.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
let user = {
name: "John",
_password: "***",
};

user = new Proxy(user, {
get(target, prop) {
if (prop.startWith("_")) {
throw new Error("접근이 제한되어 있습니다.");
}
let value = target[prop];
return typeof value === "function" ? value.bind(target) : value;
},

set(target, prop, val) {
if (prop.startWith("_")) {
throw new Error("접근이 제한됭있습니다");
} else {
target[prop] = val;
return true;
}
},

deleteProperty(target, prop) {
if (prop.startWith("_")) {
throw new Error("접근이 제한되어 잇습니다.");
} else {
delete target[prop];
return true;
}
},

ownKeys(target) {
return Object.keys(target).filter((key) => !key.startsWith("_"));
},
});

함수를 반환할 때 함수를 target으로 바인딩 해주고 있음을 주목해야 한다. 그냥 사용하려고 프록시 객체로 사용하면 원본 target 객체 메서드에서 내부 변수로 접근할때 this 는 프록시기 때문에 접근이 거부된다.

이 함수에 this를 target 원본으로 바인딩 함으로써 문제를 해결할 수 있다. 하지만 프록시로 여러번 쌓여 있다던지 하면 문제가 또 생긴다. 프록시마다 객체에 가하는 수정이 달라지는 문제도 있다..

따라서 이런 형태의 프록시는 사용하면 안된다.

has 트랩으로 ‘범위’내 여부 확인하기

1
2
3
4
let range = {
start: 1,
end: 10,
};

in 연산자를 사용해 특정 숫자가 range 내에 있는지 확인해 본다. has 트랩은 in 호출을 가로챈다.

has(target, property)

  • target: new Proxy의 첫 번째 인자로 전달되는 타깃 갳게
  • property: 프로퍼티 이름.
1
2
3
4
5
6
7
8
9
10
11
12
13
let range = {
start: 1,
end: 10,
};

range = new Proxy(range, {
has(target, prop) {
return prop >= target.start && prop <= target.end;
},
});

alert(5 in range); // true
alert(50 in range); // false

apply 랩으로 함수 감싸기

apply(target, thisArg, args) 트랩은 프락시를 함수처럼 호출하려고 할때 동작한다.

  • target: 타깃 객체
  • thisArg: this의 값
  • args: 인수 목록

일반 함수를 사용하는 데코레이터로 예시를 들어본다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function delay(f, ms) {
// 지정한 시간이 흐른 다음에 f 호출을 전달해주는 래퍼 함수를 반환한다.
return function () {
setTimeout(() => f.apply(this, arguments), ms);
};
}

function sayHi(user) {
alert(`Hello, ${user}!`);
}

// 래퍼 함수로 감싼 다음에 sayHi 를 호출하면 3초 후 함수가 호출된다.
sayHi = delay(sayHi, 3000);

sayHi("John"); // Hello, John! (3초후)

래퍼함수로 감싸고 나면 기존 함수의 프로퍼티 (name, length등 ) 정보가 사라짐.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function delay(f, ms) {
return function () {
setTimeout(() => f.apply(this, arguments), ms);
};
}

function sayHi(user) {
alert(`Hello, ${user}!`);
}

alert(sayHi.length); // 1 (함수 정의부에서 명시한 인수의 개수)

sayHi = delay(sayHi, 3000);

alert(sayHi.length); // 0 (래퍼 함수 정의부엔 인수가 없음)

Proxy 객체는 타깃 객체에 모든 것을 전달해주므로 훨씬 강력하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function delay(f, ms) {
return new Proxy(f, {
apply(target, thisArg, args) {
setTimeout(() => target.apply(thisArg, args), ms);
},
});
}

function sayHi(user) {
alert(`Hello, %${user}!`);
}

sayHi = delay(sayHi, 3000);

alert(sayHi.length); // 1 프락시는 "get length" 연산까지 타깃 객체에 전달해줍니다.

sayHi("John"); // Hello, John! (3초후)

커링

커링은 f(a, b, c) 처럼 단일 호출로 처리하는 함수를 f(a)(b)(c) 와 같이 각각의 인수가 호출 가능한 프로세스로 호출된 후 병합되도록 변환하는 것이다.

사용예시 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function curry(f) {
return function (a) {
return function (b) {
return f(a, b);
};
};
}

// usage
function sum(a, b) {
return a + b;
}

let curriedSum = curry(sum);

alert(curriedSum(1)(2)); // 3

lodash 라이브러리의 _.curry 같이 래퍼를 반환할 대 함수가 보통 때처럼 또는 partial 적으로 호출하는 것을 허용하는 더 진복적으로 구현된 커링도 있다.

1
2
3
4
5
6
7
8
function sum(a, b) {
return a + b;
}

let carriedSum = _.curry(sum); // lodash 라이브러리의 _.carry 사용

alert(carriedSum(1, 2)); // 3, 보통 때처럼
alert(carriedSum(1)(2)); // 3, partially 호출되었음

커링은 어디에 써야할까요?

예시: 로그함수

1
2
3
function log(date, importance, message) {
alert(`[${date.getHours()}:${date.getMinutes()}] [${importance}] ${message}`);
}

커링을 적용

1
log = _.curry(log);

둘다 정상 작동

1
2
log(new Date(), "DEBUG", "some debug");
log(new Date())("DEBUG")("some debug");

아래처럼 현재 시간으로 로그를 출력하는데 편리하도록 log 함수를 사용할 수 있다.

1
2
3
4
// logNow 는 log 첫 번째 인수가 고정된 partial이 될것이다.
let logNow = log(new Date());

logNow("INFO", "mesage");

디버깅 로그를 편리하게 하는 함수를 만들 수 도 있다.

1
2
3
let debugNow = logNow("DEBUG");

debugNow("message");

고급 커리 구현

다중 인수를 호용하는 고급 커리를 구현하는 방법.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function curry(func) {
return function curried(...args) {
if (args.length >= func.length) {
return func.apply(this, args);
} else {
return function (...args2) {
return curried.apply(this, args.concat(args2));
};
}
};
}

// usage
function sum(a, b, c) {
return a + b + c;
}

let curriedSum = curry(sum);

alert(curriedSum(1, 2, 3));
alert(curriedSum(1)(2, 3));
alert(curriedSum(1)(2)(3));

sum.length = 3 이다. 만약 curriedSum(1)(2)(3)을 호출하면 다음과 같이 실행된다.

  1. curriedSum(1) 이 실행될때 인수의 개수가 3보다 작으므로 else를 타서 실행된다.
  2. curriedSum(1)(2) 가 실핼될때 arg.concat을 통해 인수를 모아주었으므로 curriedSum(1, 2) 와 같고 이것도 인수의 갯수가 3보다 작으니 else를 타고 curried가 반환된다.
  3. curriedSum(1)(2)(3) 이 호출되면 인수를 다 모으고 curriedSum(1, 2, 3) 이 호출되는 것과 같다.

커리은 고정된 길이의 함수들만 사용 가능하다. f(...args) 같이 나머지 매개변수를 사용하는 함수는 이러한 방법으로 커리할 수 없다.

참조 타입

복잡한 상황에서 메서드를 호출하면 this 값을 잃어버리는 경우가 생긴다.

1
2
3
4
5
6
7
8
9
let user {
name: 'John',
hi() {alert(this.name)},
bye() { alert('Bye') }
}

user.hi();; //John

(user.name == 'John' ? user.hi : user.bye)() // TypeError

에러는 메서드를 호출할 때 thisundefined 가 할당되었기 때문에 발생했다.

왜 이런 현상이 일어날까?

(user.hi)()는 정상 작동한다. 이는 (expression)() 형태에서는 참조 값이 아닌 함수 값 자체가 전달되기 때문이다.

참조 타입 자세히 알아보기

obj.method() 엔 연사이 두개 있다.

  1. . 은 객체 프로퍼티 obj.method에 접근한다.
  2. 괄호 ()는 점근한 프로퍼티를 실행한다.

첫번째 연산에서 얻은 this가 어떻게 두번째 연산으로 전달 될까?

1
2
3
4
5
6
7
8
9
10
let user = {
nsme: "John",
hi() {
alert(this.name);
},
};

// 메서드 접근과 호출을 별도의 줄에서
let hi = user.hi;
hi(); // this 가 undefined이기 때문에 에러가 발생한다.

자바스크립트에서 user.hi()를 의도한대로 동작 시키기 위해서 .은 함수를 반환 시키는 것이 아니라 참조 타입을 반환하게 된다.
참조 타입에 속하는 값은 (base, name, strict) 이 조합된 형태이다.

  • base: 객체
  • name: 프로퍼티 이름
  • strict: 엄격 모드에서 true

user.hi()로 프로퍼티에 접근하면 함수가 아닌, 참조형(참조 타입) 값을 반환한다. 엄격 모드에선 아래와 같이 반환된다.

1
2
// 참조형 값
user, "hi", true;

참조형 값에 ()를 붙여 호출하면 객체, 객체의 메서드와 연관된 모든 정보를 얻는다. 이 정보를 기반으로 this(=user) 가 결정된다.

참조 타입은 . 연산에서 알아낸 정보를 괄호 ()로 전달해 주는 중개인 역할을 한다.

()없이 user.hi를 호출하면 점연산이 아닌 연산이 된다(할당 연산). 이때는 참조 값이 아니라 함수 만 받아서 전달하기 때문에 this정보가 사라진다. obj.method()같이 점을 사용하거나, obj[method]() 같이 대괄호를 사용해 함수를 호출했을 때만 this 값이 의도한대로 전달된다. func.bind() 등을 이용하면 해결 하는 방법도 있긴 한다.