JavaScript 동작원리
업데이트:
첫번째 내용으로 자바스크립트의 역사에 대해서 간략하게 알아보았다. 이제 히스토리는 대충 알았으니 자바스크립트라는 언어가 어떻게 동작하는지 알아야할 필요성이 있다.
자바스크립트 엔진
자바스크립트 엔진은 자바스크립트 코드를 실행하는 프로그램 또는 인터프리터 이다. 여러 목적으로 자바스크립트 엔진을 사용하지만 대체적으로 웹 브라우저에서 사용된다. - 위키백과 -
자바스크립트엔진은 주로 웹브라우저에서 사용되고있고 크롬브라우저는 C++ 로 개발된 V8엔진을 사용하고있다. 자바스크립트 엔진이 하는일은 자바스크립트 코드를 해석하고 실행시켜주는 역할을하는데 이때 가장 짧은시간내에 최적화된 코드를 생성하는것에 목표를 갖고있다.
V8 엔진 기준으로 두가지기능을 포함하고있는데 하나는 메모리힙이고 하나는 콜스택이다.
- 메모리힙: 변수나 함수등의 정보를 저장하는 공간
- 콜스택: 실행되는 코드를 순서대로 실행 FILO(First In Last Out) 으로 선입 후출 구조로 되어있다.
즉 자바스크립트는 자바스크립트 엔진에 의해서 순차적으로 실행되는 싱글스레드 기반이라는걸 알 수 있다.
const func = () => {
console.log(5)
}
console.log(1)
func()
console.log(2)
/*
1
5
2
*/
위와같은 코드는 func 라는 변수에 저장된 함수가 가장 위에 선언되어있지만 실제 호출은 console.log(1) 다음에 호출하고있기때문에 1과 2 사이에 실행결과가 나오게된다.
이렇게 동작할때 가장 문제되는 부분은 func()
라는 함수가 지금은 단순히 console 에 숫자를 찍고있지만 정말 많은 일을 수행하는 함수라고 가정했을때 해당 함수의 동작이 끝나기전까지 다음 코드인 console.log(2)
는 실행되지 않을것이다.
이러한 부분에 대한 처리를 하기위해 비동기함수를 사용하는데 대표적으로 setTimeout이 있다.
const test = () => {
console.log('test func start')
let user = null;
setTimeout(() => {
console.log('setTimeout start')
user = {
id: 'test',
name: '홍길동',
age: 26,
}
}, 1000)
console.log(user)
}
test()
위와같은 코드를 실행했을때 원래대로라면
test func start
setTimeout start
{
id: 'test',
name: '홍길동',
age: 26,
}
이렇게 결과가 나와야하겠지만
test func start
null // user
setTimeout start // 1초뒤 실행
이렇게 user에는 아무값도 들어가있지 않은상태로 출력이 된다. setTimeout 은 자바스크립트의 대표적인 비동기적함수이며 뒤에 지정한 숫자(1000 === 1초) 시간 이후에 실행되는 함수이다.
바로 이런부분이 자바스크립트가 어렵고 헷갈리고 자유분방하게 느껴지게하는것같다. 기본적으로는 실행순서가 보장되지만 특정함수나 예외적이 상황에서는 비동기적 동작이 존재하고 이 비동기적 동작을 처리하기위한 기법들이 필요하게된다.
비동기 처리
일반적으로 사용하던 비동기 처리방식에는 콜백함수를 사용하는 방법이 있다. 자바스크립트의 함수는 인자값으로 여러가지 값을 받을 수 있는데 그중에는 함수도 있다. 함수를 인자로넘겨 콜백함수로 사용하면 비동기동작하는 함수 내부에서 별도의 return 이 필요하지 않게된다.
const test = (callback) => {
console.log('test func start')
let user = null;
setTimeout(() => {
console.log('setTimeout start')
user = {
id: 'test',
name: '홍길동',
age: 26,
}
callback(user)
}, 1000)
}
test((data) => {
console.log(data)
})
위와같이 비동기적인 처리는 콜백으로 충분히 받아내어 사용 할 수 있다.
콜백지옥
위와같이 콜백을 사용하면 비동기적인 작업을 수행하는 이벤트나 서버통신 파일읽어오기 등을 원하는 출력값을 얻도록 활용할 수 있다. 저렇게 낮은 수준의 비동기 작업만 존재한다면 좋겠지만 실재로는 꽤 깊은 수준의 비동기작업을 처리할 일이 많아지는데 이때는 콜백지옥에 빠지게된다.
$.get('url', (response) => {
parseValue(response, (id) => {
auth(id, (result) => {
display(result, (text) => {
console.log(text)
})
})
})
})
위와 같이 서버와 통신을 하여 데이터를 받아오는 코드를 예시로 들었는데 통신후에 받아온 응답데이터를 처리하고, 가공하고 출력하기까지 코드가 점점 깊어지는걸 알 수 있다. 만약 저기에 파싱해야하는 데이터가 여러개이고 여러번의 파싱이 필요하다면 더욱더 가독성이 떨어지는 코드가 될것이고 어디가 어느함수의 끝인지 알기도 매우 어렵다.
콜백지옥 탈출 - 외부선언
이러한 콜백지옥을 탈출하는 방법중 한가지는 콜백함수들을 외부에서 사용할 수 있도록 하는 방법이다.
function parseValueDone (id) {
auth(id, authDone);
}
function authDone(result) {
display(result, displayDone);
}
function displayDone(text) {
console.log(text);
}
$.get('url', (response) => {
parseValue(response, parseValueDone)
})
위와같이 내부에서 실행하던 콜백함수를 외부로 선언하여 사용하면 콜백지옥을 해결할 수 있지만 함수가 많아질수록 디버깅을 할때 계속 찾아들어가야하는 비효율성의 문제가 아직 남아있다.
콜백 지옥 탈출 - Generator (ES6)
Generator 는 ES6에서 도입된 함수로 이터러블을 생성하는 함수이다. 즉 반복가능한 함수를 생성하여 사용할 수 있도록 해준다.
function* count() {
console.log('호출 1')
yield 1;
console.log('호출 2')
yield 2;
}
const generatorCount = count();
console.log(generatorCount.next()) // 호출 1 { value: 1, done: false }
console.log(generatorCount.next()) // 호출 2 { value: 1, done: false }
console.log(generatorCount.next()) // { value: undefined, done: true }
위와같이 기존의 자바스크립트 함수와는 좀 다르게 사용이 되는데 함수가 한번 호출되고 사라지는것이아닌 호출되고나서 yield 를 만난시점에서 잠시 중단시키고 다시한번 next() 를 호출하면 그 다음 yield에서 실행이 중되는 방식으로 value 에는 yield 에 적었던 값이 그리고 done 에는 반복의 아직 남았는지 끝인지 상태를 알려주는 객체를 리턴하게된다.
이러한 특징을 활용하여 제너레이터 함수를 사용하여 비동기의 동작을 동기적으로 제어 할 수 있다.
// fetch 사용
function getUser(gen, username) {
fetch(`https://api.github.com/users/${username}`)
.then(res => res.json())
.then(user => gen.nect(user.name));
}
const newGen = (function* () {
let user;
user = yield getUser(newGen, 'kang');
console.log(user) // "Brian Kang"
user = yield getUser(newGen, 'kim');
console.log(user) // "Kim Altintop"
}());
newGen.next();
위와같이 비동기적인 처리를 제너레이터함수를 사용하여 처리 할 수 있다. 그러나 여전히 코드는 보기 불편하고 라인수가 길어진다.
콜백 지옥 탈출 - Promise
제너레이터를 이용하여 비동기함수를 처리할 수 있지만 여전히 코드는 가독성이 안좋고 심지어 지원되는 브라우저도한정되어있어 편안하게 쓸수있는 방법은 아니다. 저런방법과 문법도 있다 정도만 알고있으면 되지않을까 싶다. 그래서 공식적으로 많이 사용하는 방법이 바로 이 프로미스다.
ES6에서 도입되었고 비동기 처리시점을 명확하게 표현할 수 있는 장점이 있다.
const promise = new Promise((resolve, reject) => {
// 비동기 작업 진행
if (성공한다면) {
resolve('result');
}
if (실패한다면) {
reject('fail')
}
})
Promise 는 위와같이 사용할 수 있으며 프로미스는 진행상태에 따라 결과에따라 상태를 가지게된다.
- pending : 비동기처리가 아직 수행되지않음
- fulfilled : 비동기수행 성공
- rejected : 비동기 수행 실패
promise 이후처리는 then 으로 할 수 있는데 이미 제너레이터 예제를 다루면서 이와관련한 함수를 사용했었는데 바로 fetch()
이다. fetch()
는 Promise 타입의 객체를 반환하기때문에 then으로 성공시 응답을 받아오고 catch(err)
로 실패했을때 객체를 받아온다.
실제 비동기함수를 사용하는경우는 대부분이 서버에 요청을 날리고 응답을 받아올때 사용하는데 Promise 객체를 그대로 사용하는것이 아닌 fetch()
같은 라이브러리를 활용하는 경우가 더 많다.
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then(reponse => {
return response.json();
})
.then(result => {
console.log(result)
})
/*
{
completed: false
id: 1
title: "delectus aut autem"
userId: 1
}
*/
response.json()
에서 받아온 응답객체에 json() 메소드를 사용하였는데 이는 response 의스트림이 완료될때까지 읽고 다 읽고다면 body 의 텍스트를 Promise 형태로 반환시켜준다.
여기서 의문점
분명 자바스크립트는 싱글스레드이고 그렇기때문에 한번에 하나의 동작만 가능하다. 그래서 어떤 하나의 동작이 끝나야지만 다음 코드가 실행되는 형태인데 어떻게 이런 비동기적인 동작이 자바스크립트에서 가능한거고 존재하게 된건지 의문이 들 수 있다.
일단 이러한 비동기적 동작이 가능하게 된 계기는 Ajax의 등장으로 가능해졌고 Ajax 가 등장하기 전에는 웹페이지가 서버로부터 완전한 HTML을 전송받아 웹페이지 전체를 렌더링하는 방식으로 동작했고 화면이 전환될때마다 처음부터 다시 로드하게되는 비효율적인 상황이 발생하였고 이를 해결하기위해 Ajax가 등장하게 되었다.
fetch 도 Ajax 를 구현하는 기술중 하나이고 이 기술은 Web api 에서 제공하고있다. 즉 자바스크립트는 웹 브라우저와 상호작용을 하며 동작하는데 이러한 비동기적인 요청과 작업은 웹브라우저에서 동작하게된다.
콜백 지옥 탈출 - async / await
가장 최근에 나온 비동기 처리방법으로 아주 간단하게 사용가능하다.
async function asyncTest() {
const response = await fetch('https://jsonplaceholder.typicode.com/todos/1')
const result = await response.json();
console.log(result)
}
asyncTest()
최상위레벨에 async 로 선언하여 함수를 작성하고 순서를 보장하고싶은 실행함수앞에 await를 붙여주기만하면된다.
async function 을 promise 를 사용하여 풀이하면 아래와같다.
function promiseFunction() {
return new Promise((resolve) => {
resolve();
})
}
await 를 사용하면 Promise 의 resolve 값이 반환된다. async/await 를 사용하면 예외처리는 try/catch 로 처리가 가능하다.
async function asyncTest() {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/todos/1')
const result = await response.json();
console.log(result)
} catch(err) {
console.log(err)
}
}
asyncTest()
위와같은형태가 일반적으로 서버에 요청을 보낼때 사용하는 제일 간단한 형태이고 가장 많이 사용하는 패턴이기도하다. 대충 알고 사용하는데에도 어려움은없다. 그러나 정확히 어떻게 발전해왔고 어떻게 동작하는지에대한 정확한 인지가 있어야지만 문제가 발생했을때 그걸 해결 할 수있다고 생각한다. 어떤 기능이나 라이브러리를 사용하는건 누구나 할 수 있다. 하지만 그에따른 예외처리나 에러상황시 그것을 어떻게 대처하는지는 기본과 뼈대를 알아야지만 가능한것같다.
댓글남기기