JavaScript
고친 사람 github-actions[bot]
JavaScript 는 웹 페이지를 눌렀을 때 화면이 바뀌게 해 주는 프로그래밍 언어입니다. 브라우저가 이 언어를 알아들어서, 따로 설치하지 않아도 페이지에 딸려 온 코드가 곧바로 돕니다. 같은 언어를 브라우저 밖으로 꺼내 서버 프로그램을 짜기도 합니다.
쉽고 빠른 이해
JavaScript 는 브라우저가 받아서 바로 실행하는 언어입니다. 장바구니 버튼을 눌렀을 때 페이지를 새로 받아오지 않고 담긴 개수만 바뀌는 것이 이 언어가 하는 일입니다.
이게 없으면 화면의 작은 변화 하나에도 서버가 페이지를 다시 만들어 보내야 합니다. 누를 때마다 기다리는 시간이 생깁니다.
어떻게 도는가:
- 브라우저가 페이지를 받으면서 거기 딸린 스크립트도 같이 받습니다
- 브라우저 안에 들어 있는 엔진이 그 코드를 읽어 바로 돌립니다
- 코드가 화면을 이루는 요소를 찾아 내용이나 모양을 바꿉니다
대가도 있습니다. 코드를 미리 검사하는 단계가 없어서, 타입이 안 맞는 대입은 실행이 거기까지 가서야 드러납니다. 실행 흐름이 하나뿐이라 계산이 오래 걸리는 코드 한 덩이가 나머지 일을 모두 세웁니다. 그래서 화면을 다루는 코드와 기다림이 대부분인 서버에 맞습니다. 한 요청이 계산에 오래 매달리는 서버에는 안 맞습니다.
상세
이 절은 JavaScript 를 다섯 가지로 나눠 봅니다.
- 소스는 무엇을 거쳐 실행까지 가나
- 값의 타입은 언제 정해지나
- 객체는 어떻게 다른 객체의 것을 쓰나
- 흐름이 하나뿐인데 기다리는 일은 어떻게 하나
- 브라우저 밖에서는 무엇이 달라지나
옆에 두고 견주는 상대는 Java 나 Go 처럼 미리 빌드해서 서버에 올리는 언어입니다. JavaScript 의 규칙은 대부분 받자마자 실행해야 하는 환경에서 나왔습니다. 그쪽 모습을 옆에 두면 각 규칙이 왜 그런지가 드러납니다.
소스에서 실행까지
JavaScript 코드는 .js 로 끝나는 텍스트 파일에 적습니다. 이 파일은 따로 빌드하지 않고, 소스를 그대로
받아 읽고 실행하는 프로그램에 넘깁니다.
브라우저가 이 코드를 알아듣는다는 것은 브라우저 안에 그 코드를 실행하는 프로그램이 들어 있다는 뜻입니다. 소스를 받아 바로 실행하는 이런 프로그램을 인터프리터라고 부릅니다. JavaScript 를 실행하는 인터프리터의 이름이 자바스크립트 엔진이고, 아래에서는 줄여서 엔진이라고 적습니다.
엔진이 브라우저에 딸려 오기 때문에 웹 페이지 쪽에서는 실행 환경을 따로 깔 일이 없습니다. 빌드 단계가 없다는 것은 두 가지를 뜻합니다. 코드를 고치고 페이지를 새로 고치면 바로 반영됩니다. 반대로 틀린 곳이 있어도 실행이 그 줄에 닿기 전에는 아무도 모릅니다.
엔진이 소스를 한 줄씩 해석하기만 하는 것은 아닙니다. 자주 도는 함수를 골라 실행 도중에 기계어로 바꿔 두고 그 다음부터는 바꿔 둔 것을 씁니다. 이렇게 돌리면서 필요한 부분만 기계어로 바꾸는 것을 JIT 컴파일(Just-In-Time compilation, 실행 중 컴파일)이라고 부릅니다.
flowchart TD
J[".js 소스"] -->|"빌드 단계 없이 그대로"| E
subgraph BR["브라우저"]
E["자바스크립트 엔진"] --> R["한 줄씩 해석해 실행"]
R --> H["자주 도는 함수"]
H --> M["기계어로 바꿔 둔다 · JIT"]
M --> U["다음부터는 바꿔 둔 것을 쓴다"]
end
브라우저는 스크립트를 실행하기 전에 받아 온 문서를 객체 트리로 만들어 둡니다. 화면에 무엇이 어떤 순서로 들어 있는지를 적은 문서가 HTML(HyperText Markup Language, 하이퍼텍스트 마크업 언어)입니다. 그 문서를 객체로 바꿔 둔 트리가 DOM(Document Object Model, 문서 객체 모델)입니다.
스크립트가 화면을 바꾸는 일은 전부 이 트리를 고치는 일입니다. 코드가 트리에서 요소 하나를 찾아 글자를 바꾸면, 브라우저가 그 변화를 보고 화면을 다시 그립니다.
flowchart TD
HD["HTML 문서"] -->|"객체 트리로 만든다"| D
subgraph DM["DOM"]
D["문서"] --> P["문단"]
D --> B["버튼"]
end
S["스크립트"] -->|"요소 하나를 고친다"| P
P -->|"바뀌면"| V["브라우저가 화면을 다시 그린다"]
실행 중에 정해지는 값의 타입
변수를 만들 때 타입을 적지 않습니다. 낱말 둘로 만듭니다. let 은 나중에 다른 값을 다시 넣을 수 있는
변수입니다. const 는 한 번 넣으면 못 바꾸는 변수입니다.
한 변수에 성격이 다른 값을 차례로 넣어도 막지 않습니다. 타입을 가지는 것은 변수가 아니라 값 쪽이기 때문입니다.
let x = 3;
x = "hello"; // 막지 않는다
console.log(typeof x); // string
이렇게 값이 자기 타입을 들고 다니고 검사도 실행할 때 하는 방식을 동적 타입이라고 부릅니다. 타입이 없다는 뜻이 아닙니다. 타입을 확인하는 시점이 빌드가 아니라 실행이라는 뜻입니다.
대가는 타입이 안 맞을 때 드러납니다. 멈추고 알려 주는 대신 한쪽 값을 다른 쪽 타입으로 바꿔서 계속 갑니다. 아래 두 줄은 같은 두 값을 놓고 서로 다른 답을 냅니다.
console.log(1 + "2"); // "12"
console.log(1 - "2"); // -1
+ 는 수를 더하는 데도 쓰고 글자를 잇는 데도 씁니다. 한쪽이 문자열이면 잇는 쪽으로 정하고 다른 쪽을
문자열로 바꿉니다. - 에는 잇는다는 뜻이 없어서 양쪽을 수로 바꿉니다.
flowchart TD
A["연산자가 + 인가"] -->|"그렇다"| C["한쪽이 문자열인가"]
A -->|"아니다 · -"| N["양쪽을 수로 바꾼다 · -1"]
C -->|"그렇다"| T["문자열로 맞춰 잇는다 · 「12」"]
C -->|"아니다"| P2["수로 더한다"]
이처럼 필요한 타입으로 값을 소리 없이 바꾸는 것을 암묵적 형변환이라고 부릅니다. 서버 코드에서 숫자를 기대한 계산이 문자열을 받으면 오류 대신 엉뚱한 값이 흘러갑니다. 코드가 커지면 이 문제를 미리 잡으려고 타입을 적는 TypeScript 를 얹어 빌드 단계를 만들어 붙이기도 합니다.
객체를 물려받는 프로토타입 사슬
JavaScript 의 객체는 이름과 값의 짝을 모아 둔 것입니다. 중괄호 안에 짝을 적으면 그것이 객체가 됩니다. 클래스를 미리 만들어 둘 필요가 없습니다.
const user = { id: 1, name: "eun" };
console.log(user.name); // eun
객체마다 자기보다 위에 있는 객체를 하나 가리킵니다. 그렇게 가리켜진 위쪽 객체를 프로토타입이라고 부릅니다. 어떤 이름을 찾을 때 자기한테 없으면 프로토타입으로 올라가 거기서 다시 찾습니다.
예를 들어 위의 user 를 글자로 찍어 보면 엔진은 그 객체에서 toString 이라는 함수를 찾습니다. 객체를
글자로 바꿔 주는 함수입니다.
이 함수는 user 자기한테는 없고 한 칸 위 객체에 놓여 있습니다. 그래서 객체마다 이 함수를 따로 가지지
않습니다. 사슬을 타고 같은 것을 씁니다.
사슬은 위로 올라가다 아무것도 안 가리키는 데서 끝납니다. 끝까지 못 찾으면 값이 없다는 뜻의
undefined 가 나옵니다.
flowchart TD
U["user · id · name"] -->|"없으면 위로"| O["프로토타입 · toString"]
O -->|"없으면 위로"| E2["사슬 끝 · 가리키는 것이 없다 · 여기까지 못 찾으면 undefined"]
클래스를 적는 문법도 있습니다. 다만 그 문법이 만드는 것도 결국 이 사슬입니다. 클래스는 사슬을 짜는 일을 짧게 적게 해 주는 껍데기입니다.
하나뿐인 실행 흐름과 이벤트 루프
JavaScript 코드는 한 번에 한 줄씩, 하나의 흐름으로만 돕니다. 여러 스레드에 일을 나눠 주지 않습니다. 스레드는 운영체제가 번갈아 시간을 나눠 주는 실행 흐름입니다.
흐름이 하나면 잠금을 걸 일이 없어집니다. 두 코드가 같은 값을 동시에 고치는 상황 자체가 안 생기기 때문입니다. 대신 기다리는 동안 그 흐름이 붙잡혀 있으면 화면 전체가 멈춥니다.
그래서 시간이 걸리는 일은 엔진이 직접 붙들지 않습니다. 네트워크 응답이나 타이머 같은 것은 바깥에 맡깁니다. 이때 끝나면 불러 달라며 함수를 하나 같이 넘깁니다. 이렇게 나중에 불러 달라고 넘기는 함수를 콜백이라고 부릅니다.
맡긴 일이 끝나도 콜백이 곧바로 실행되지는 않습니다. 차례를 기다리며 작업 큐에 줄을 섭니다. 지금 돌고 있는 코드가 쌓이는 곳은 따로 있습니다. 그곳이 호출 스택입니다.
이 둘 사이를 오가는 장치가 이벤트 루프입니다. 스택이 비었는지 되풀이해 보고, 비면 큐 맨 앞의 콜백을 하나 꺼내 스택에 올립니다.
flowchart TD
S["호출 스택 · 지금 도는 코드"] -->|"기다릴 일을 맡긴다"| W["바깥 · 타이머 · 네트워크"]
W -->|"끝나면 콜백을 넣는다"| Q["작업 큐 · 차례를 기다리는 콜백"]
L["이벤트 루프"] -->|"스택이 비었나 본다"| S
L -->|"비었으면 맨 앞 하나를 꺼내 올린다"| Q
콜백을 여러 개 이어 붙이면 함수 안에 함수가 겹겹이 들어가 읽기 힘들어집니다. 그래서 아직 안 나온 결과를
미리 대신 들고 있는 객체를 두었습니다. 이 객체가 프로미스입니다. 그 결과가 나올 때까지 기다리는
문장은 await 로 적습니다.
아래는 주소 하나를 받아 데이터를 가져오는 함수입니다. fetch 는 요청을 보내는 함수입니다. json 은
받은 본문을 객체로 바꿔 주는 함수입니다.
둘 다 프로미스를 돌려주므로 앞에 await 가 붙습니다. await 를 쓰는 함수는 머리에 async 를 적어
기다릴 수 있는 함수라고 표시합니다.
async function load(url) {
const res = await fetch(url); // 응답을 기다린다
return await res.json(); // 본문을 객체로
}
await 는 기다리는 동안 흐름을 내놓습니다. 그 사이 이벤트 루프가 다른 콜백을 돌립니다. 코드는 위아래로
이어져 있지만, 도는 동안 그 두 줄 사이에 다른 일이 끼어듭니다.
sequenceDiagram
participant 코드
participant 엔진
participant 바깥
코드->>엔진: fetch 를 맡긴다
엔진->>바깥: 요청을 보낸다
Note over 엔진: 흐름을 내놓고 큐의 다른 콜백을 돌린다
바깥-->>엔진: 응답이 왔다
엔진-->>코드: 다음 줄로 · json
Note over 코드,엔진: 계산이 오래 걸리면 스택이 안 비어 큐가 그대로 선다
이 방식이 감당 못 하는 것이 하나 있습니다. 바깥에 맡길 수 없는 일, 그러니까 계산 자체가 오래 걸리는 코드입니다. 그 코드가 도는 동안에는 스택이 비지 않아서 큐에 쌓인 콜백이 하나도 못 올라갑니다.
브라우저 밖의 실행 환경
같은 언어를 서버에서도 쓰려고 엔진만 떼어 내 만든 실행 환경이 Node.js 입니다. 언어는 바뀌지 않습니다. 둘레에 놓이는 물건이 달라집니다.
브라우저 쪽에서 쓰던 화면 관련 기능은 여기 없습니다. 페이지가 없으니 DOM 도 없습니다. 대신 파일을 읽고 쓰거나 포트를 열어 요청을 받는 기능이 붙습니다.
flowchart TD
subgraph B2["브라우저"]
E3["엔진"]
D2["DOM · 화면"]
end
subgraph N2["Node.js"]
E4["엔진"]
F2["파일 · 포트"]
end
E3 -.->|"같은 엔진"| E4
앞 소절의 실행 모델은 서버에서도 그대로입니다. 요청 하나마다 스레드를 하나씩 붙이지 않고, 한 흐름이 요청 여럿을 번갈아 처리합니다. 대부분의 시간을 데이터베이스나 다른 서버의 응답을 기다리며 보내는 서버라면 기다리는 동안 흐름을 내주므로 이 방식이 맞물립니다.
고른 것과 내준 것
위 다섯은 저마다 무언가를 내주고 얻은 것입니다. 한데 모으면 이렇습니다.
| 고른 것 | 얻는 것 | 내준 것 |
|---|---|---|
| 소스를 받아 바로 실행한다 | 고친 코드를 새로 고침 한 번으로 확인한다 | 빌드할 때 걸러질 오류가 실행 중에 드러난다 |
| 변수에 타입을 안 적는다 | 값의 모양이 바뀌어도 선언을 안 고친다 | 타입이 안 맞아도 멈추지 않고 값을 바꿔 버린다 |
| 객체가 객체를 직접 물려받는다 | 클래스를 미리 정하지 않고 객체를 만든다 | 어떤 이름이 어디서 왔는지 사슬을 따라가야 안다 |
| 실행 흐름을 하나로 둔다 | 잠금 없이 공유된 값을 다룬다 | 계산이 오래 걸리는 코드 하나가 나머지를 세운다 |
| 기다리는 일을 바깥에 맡긴다 | 기다리는 동안 다른 요청을 처리한다 | 코드에 적힌 순서와 도는 순서가 갈린다 |
그래서 화면을 다루는 코드에는 JavaScript 를 먼저 고릅니다. 서버에서는 기다리는 일이 대부분인 쪽에 고릅니다. 브라우저에서 이미 이 언어를 쓰고 있으면 서버까지 한 언어로 맞출 수 있다는 것도 고르는 이유가 됩니다.
반대로 한 요청이 계산에 오래 매달리는 서버나, 타입이 틀린 곳을 빌드에서 걸러야 하는 큰 코드베이스에는 값이 덜합니다. 앞의 경우에는 계산을 다른 프로세스로 내보내야 합니다. 뒤의 경우에는 타입을 적는 도구를 얹어 빌드 단계를 다시 만들어야 합니다.
관련 항목
JavaScript 소스가 실행되기까지 거치는 구성 요소
자바스크립트 엔진 · 인터프리터 · JIT 컴파일 · 바이트코드 · 가비지 컬렉션 · 런타임
JavaScript 의 실행 흐름을 설명하는 개념
이벤트 루프 · 호출 스택 · 작업 큐 · 콜백 · 프로미스 · 비동기 함수 · 논블로킹 입출력 · 동시성 · 스레드
JavaScript 언어를 이루는 문법 요소
프로토타입 · 클로저 · 암묵적 형변환 · 템플릿 리터럴 · 구조 분해 할당 · 화살표 함수 · 모듈
JavaScript 가 브라우저에서 다루는 대상
DOM · 브라우저 · HTML · CSS · Web API · 렌더링 · 이벤트 핸들러
JavaScript 를 브라우저 밖에서 실행하는 환경
Node.js · Deno · Bun · 서버 사이드 렌더링 · WebAssembly
JavaScript 위에 타입을 얹는 도구와 개념
TypeScript · 동적 타입 · 정적 타입 · 타입 추론 · 트랜스파일
JavaScript 코드를 묶고 배포하는 도구
JavaScript 로 화면을 짓는 프레임워크
React · Vue · Angular · Svelte · Next.js
JavaScript 가 서버와 주고받을 때 쓰는 형식과 규약
JSON · HTTP · API · REST · GraphQL · WebSocket
서버 개발을 두고 JavaScript 와 겨루는 프로그래밍 언어
Java · Python · Go · Kotlin · C · Ruby · PHP
JavaScript 가 속하는 상위 분류
프로그래밍 언어 · 스크립트 언어 · ECMAScript · 객체 지향 프로그래밍 · 함수형 프로그래밍
다른 이름: 자바스크립트 · JS