티스토리 뷰
2024년 09월 원티드 프론트엔드 프리온보딩 간단 요약 - 이벤트 루프
이벤트 루프란?
인터넷에서 `이벤트 루프`에 대해 검색해보면 다음과 같은 내용이 나온다.
`비동기`적으로 실행되는 작업들을 관리하고, `콜 스택`에 함수가 쌓이고, `비동기` 함수는 `큐`에 쌓는다. 자바스크립트의 `비동기 처리`를 관리하는 매커니즘이고, `싱글스레드` 언어 자바스크립트가 `멀티 스레드`로 작업을 동시에 할 수 있게 도와주는 것이다.
`이벤트 루프`를 정의하자면, 싱글 스레드로 동작하는 자바스크립트를 브라우저에서 동시성을 제공하기 위한 동작 방식을 의미한다. 여기서 자바스크립트는 동기적으로 작동하는 언어이다.
여기서! 자바스크립트가 싱글 스레드인 것과 자바스크립트 런타임이 싱글스레드인 것이 같다고 생각하면 안 된다.
동작 방식
*기본 동작 방식*
1번 요청 → Call Stack으로 이동 → 1번 출력 → 2번 요청 → Call Stack으로 이동 → 2번 출력
*setTimeout*
1번 요청 → Call Stack으로 이동 → 1번 출력 → 2번 setTimeout 요청 → Call Stack으로 이동 → 2번 setTimeout 요청 Web APIs으로 이동 → 3번 요청 → Call Stack으로 이동 → 3번 출력 → 2번 setTimeout 요청 Callback Queue로 이동 → Call Stack으로 이동 → 2번 setTimeout 출력(시간이 됐을 때)
이벤트 루프 목록
- async/await
- callback
- Promise
- setTimeout
- fetch
비동기/동기 프로그래밍
`비동기(Asynchronous)`와 `동기(Synchronous)`는 기술이 아닌 개념적인 내용으로 알고 있어야 한다.
- 동기(Synchronous): 작업이 순차적으로 진행되는 것 / 작업이 끝날 때까지 '내가' 기다리는 것
- 비동기(Asynchronous): 작업이 순차적으로 진행되지 않는 것 / 작업이 끝날 때까지 '내가' 기다리지 않는 것
// 동기
function fn_1() {
return "1";
}
function fn_2(data: string) {
return `${data}_2`;
}
function fn_3(data: string) {
return `${data}_3`;
}
const data_1 = fn_1(); // data_1 = "1"
const data_2 = fn_2(data_1); // data_2 = "1_2"
const data_3 = fn_3(data_2); // data_3 = "1_2_3"
// 비동기
function fn_1() {
return "1";
}
function fn_2(data: string) {
return `${data}_2`;
}
function fn_3(data: string) {
return `${data}_3`;
}
const data_1 = fn_1(); // data_1 = "1"
const data_2 = fn_2(data_1); // data_2 = "undefined_2"
const data_3 = fn_3(data_2); // data_3 = "undefined_undefined_3"
블로킹(Blocking)과 논블로킹(Non-Blocking)
- 블로킹(Blocking): 요청에 대한 결과값을 받을 때까지 기다리는 것
- 논블로킹(Non-Blocking): 요청에 대한 결과값을 받을 때까지 기다리지 않는 것
// 블로킹 예시
OO를 바꿔주세요 → 원하는 결과가 돌아올 때까지 대기 → 원하는 결과 반환
// 논블로킹 예시
OO를 바꿔주세요 → 아직 없으니 대기하세요 → OO를 바꿔주세요 → 아직 없으니 대기하세요 → OO를 바꿔주세요 → 원하는 결과 반환
그렇다면, `동기/비동기`와 `블로킹/논블로킹`과 똑같은 게 아닌가에 대해 생각하게 된다.
비슷해 보이지만, 자세히 보면 다르다.
`동기/비동기`는 요청한 작업이 끝날 때까지 내가 기다리고 다음 작업을 시작할 것인가? 요청한 작업을 내가 기다릴 것인가?
`블로킹/논블로킹`은 요청한 작업을 위해 지금 작업을 멈출 것인가? 요청한 작업이 끝날 때까지 현재 작업을 멈출 것인가?
주체가 `나`인가? `상황(작업 방식)`인가에 대한 차이가 존재한다.
`동기/블로킹` 방식을 사용하면 `내가` 요청을 하면 작업이 끝날 때까지(작업 방식) 기다리게 된다. 즉, `동기` 방식이기 때문에 "내가" 기다리고 있는 것이다. 이는, "내가" 요청을 하면 작업이 끝날 때까지 추가 요청을 하지 않고 작업이 끝날 때까지 "내가" 계속 기다리고 있는 것이다.
`동기/논블로킹` 방식을 사용하면 "내가" 요청을 하고 작업이 끝날 때까지(작업 방식) 기다리지 않는다. 즉, "내가" 요청을 하고, 요청한 작업이 완료될 때까지 주기적으로 추가 요청을 계속 보낸다. 그리고 작업이 완료되지 않았다면 그에 대한 결과를, 마지막에 요청 작업이 완료되면 그 결과를 "내가" 받게 된다.
`비동기/논블로킹` 방식은 조금 어렵다.
| ---요청(callback 등록)--------→ | ---요청(callback 실행)--→ |
| ←--결과(callback 등록 완료)--- | |
여기서 요청은 "내가" 하지만, 기다리는 쪽은 요청자인 "내가"아니다. 중간에 기다리는 요청자가 기다리게 된다. 그리고 작업이 끝날 때까지 기다리지 않는다.
싱글스레드(Single Thread)와 멀티스레드(Multi Thread)
자바스크립트는 `싱글 스레드(Single Thread)`이다.
그렇다면, `싱글 스레드(Single Thread)`는 무엇일까?
그 전에 `스레드(Thread)`는 무엇일까?
`스레드(Thread)`는 어떤 프로그램 내에서 특히 프로세스 내에서 실행되는 흐름 단위를 말한다고 인터넷에 정의되어 있다. 또한, 한 프로그램은 하나의 스레드를 가지고 있지만, 프로그램 환경에 따라 둘 이상의 스레드를 동시에 실행할 수 있다고 나타나 있다. 이것이 `멀티 스레드(Multi Thread)`이다.
그렇다면, 프로그램/프로세스/스레드가 무엇일까?
프로그램은 컴퓨터가 어떤 작업을 수행하기 위해 모아둔 명령어 집합체이며, 컴퓨터가 World Wide Web에 접근하기 위한 명령어의 모음집이다.
프로세스는 프로그램 내의 `Input/Output`, `Schedule`, `Fetch`, `Animation` 등이 작업들이며, 이 작업들의 흐름을 스레드라고 한다. 만약, 프로세스의 작업들이 순차적으로 진행된다면 이것이 `싱글 스레드`이고, 반대로 동시에 진행된다면 `멀티 스레드`가 된다.
*프로세스*
싱글 스레드
Input/Output → Schedule → Fetch → Animation
멀티 스레드
Input/Output -→
Schedule ---→
Animation --→
Fetch ----→
병럴성(Parallelism)과 동시성(Concurrency)
`병렬성 (Parallelism)`은 동시에 여러 작업을 처리하는 것을 말한다.
`동시성(Concurrency)`은 동시에 여러 작업을 처리하는 것처럼 보이는 것을 말한다.
`병렬성`을 이미지로 확인해보자면,
`동시성`을 이미지로 확인해보자면,
이미지에서 보이듯이, `병렬성`은 두 개의 Task가 동시에 처리하고 있는 것을 확인할 수 있고, `동시성`은 동시에 처리하는 것은 아니지만, 동시에 처리하고 있는 것처럼 진행되고 있음을 확인할 수 있다.
자바스크립트
`자바스크립트`는 싱글 스레드이다. 그렇다면, 왜 싱글 스레드를 선택했을까?
1995년 클라이언트 사이드에서 동적인 Interaction을 지원하는 스크립트 언어로써 브라우저에서 작동하기를 원했었다. 당시 시대를 보면 전체적으로 지금과 달리 가격이 비쌌고, 기술적인 부분도 부족함이 존재했다.
그래서 왜 `싱글 스레드`를 선택했을까? 이유는 `싱글 스레드`는 간단하고, 구현이 쉬워 빠르고, 필요한 자원이 적어 가볍기 때문이다. 이러한 이유로 자바스크립트는 싱글 스레드를 선택하였다.
정리
- 프로그램(Program): 컴퓨터가 작업하기 위한 명령어 집합체
- 프로세스(Process): OS에 자원을 할당받은 프로그램이 실행되어 동적인 상태
- 스레드(Thread): 프로세스 내에서 실행되는 여러 흐름 단위
- 동기(Synchronouse): 작업이 순차적으로 진행. 작업이 끝날 때까지 내가 대기
- 비동기(Asynchronous): 작업이 순차적으로 진행되지 않음. 작업이 끝날 때까지 내가 기다리지 않음
강의 내용에서 `동기`, `비동기`, `블로킹`, `논블로킹`에 대해 이해되지 않는 부분을 조금 더 이해하기 위해서 학습했다. 강의는 시간이 정해져 있기 때문에 이해에 필요한 시간이 부족하였기 때문에 스스로 찾아봤다.
쉽게 이해해보자. `동기`와 `비동기`는 작업을 요청하면 완료를 기다리냐 기다리지 않느냐 이다.
`동기`를 예시로 들면, 친구에게 이메일을 보내고, 답장이 올 때까지 계속 기다리면서 다른 일을 하지 못하는 것이다.
`비동기`는 친구에게 이메일을 보내고, 답장이 올 때까지 기다리지 않고 다른 일을 하다가 답장이 오면 알람을 받는 것이다.
`블로킹`과 `논블로킹`은 시스템 자원을 요청하면 그 자원이 사용 가능해질 때까지 프로그램이 기다리냐 아니면 사용 가능하지 않은 상태이면 기다리지 않고 다음 작업을 처리하냐 이다.
`블로킹`을 예시로 들면, 자판기에서 음료수를 뽑을 때, 앞사람이 음료수를 뽑고 있다면 기다리고 있는 것이다.
`논블로킹`은 자판기에 음료수를 뽑으러 갔는데 앞 사람이 사용중이면 다른 일을 하러 갔다가 나중에 자판기가 비어있을 때 다시 음료수를 뽑으러 가는 것이다.
특히, 가장 이해가 어려운 `비동기/논블로킹` 방식을 이해해보자.
`비동기/논블로킹` 방식에서 요청을 보내면 그 작업이 완료될 때까지 기다리지 않고 바로 다른 일을 하면서 자원이 준비되면 그 결과를 받아서 처리하는 방식이다. `비동기적`이기 때문에 작업이 끝날 때까지 기자리지 않고 바로 다른 작업을 진행하고, `논블로킹`이기 때문에 자원이 준비되지 않아도 기다리지 않고 다른 일을 하다가, 자원이 준비되면 그 때 자원을 사용한다.
예시를 들자면, 친구에게 이메일을 보내고 답장이 올 때까지 기다리지 않고 다른 작업을 하고, 이메일을 보낼 때도, 서버가 바빠 바로 전송되지 않더라도 기다리지 않고 다음 작업을 처리하고 있는 것이라고 이해하면 된다. 웹 서버를 예로 들자면, 웹 서버는 많은 사용자의 요청을 받는다. `비동기/논블로킹`의 서버는 각각 요청이 처리될 때까지 기다리지 않고, 다른 요청을 계속 받으면서 기존 요청들의 결과가 도착하면 그 때 처리한다.
// 비동기 + 논블로킹 예시
console.log("작업 A 시작");
// 비동기 요청 (네트워크 요청, 파일 읽기 등)
setTimeout(() => {
console.log("작업 B 완료");
}, 2000);
console.log("작업 C 시작 및 완료");
// 출력 순서:
// 작업 A 시작
// 작업 C 시작 및 완료
// (2초 후) 작업 B 완료
위 코드에서 `setTimeout`은 비동기적이고 논블로킹 방식이다. `setTimeout`이 실행되는 동안 2초를 기다리지 않고, 다음 코드인 `작업 C`가 실행되고 있기 때문이다.
Queue와 Stack
`Queue`, `Array`, `Stack`, `Graph`, Tree`의 공통점은 자료구조이라는 것이다.
`자료구조`는 데이터를 효율적으로 저장하고 관리하기 위한 방법 또는 형식을 말한다. 쉽게 말해서, 컴퓨터가 데이터를 잘 저장하고, 필요할 때 빠르게 찾거나 변경할 수 있도록 돕는 틀 또는 구조이다.
저장 공간
하드디스크(Hard Disk) | Memory | |
가격 | 저렴하다 | 비싸다 |
용량 | 많다 | 적다 |
성능 | 느리다 | 빠르다 |
만약, 크롬 브라우저 동작을 가정한다면, 하드디스크에서 프로그램이 실행되고, 메모리에서 프로세스가 진행된다.
용량이 적은 메모리를 효율적으로 사용해야 하는 게 중요한 점이다.
비트/바이트 | 2진법/16진법
`2진법`은 `0`과 `1`로 이루어져 있다.
`1비트`는 어떻게 구성되어 있을까? `0` 또는 `1`으로 이루어져 있다. 그렇다면 `2비트`는 어떻게 되어 있을까? `00`, `01`, `10`, `11`으로 이루어질 것이다. 이렇게 `8비트`가 되면 `1바이트`가 된다.
그렇다면, `16진법`은 무엇일까? 우리가 색상표를 볼 때 많이 볼 수 있는 것이 `#FF0000`같은 것을 많이 봤을 것이다. 이것이 16진법이라고 생각하면 된다. 우리가 볼 때는 간단하게 이렇게 표현되지만, 16진수로는 `0xFF0000FF`로 표현된다.
- 0x: 16진수(Hexadecimal)
- FF: Red
- 00: Green
- 00: Blue
- FF: 16 * 16
메모리
- Code → 실행한 프로그램의 코드 영역
- Data → 전역 변수, 정적 변수 영역
- Heap → 동적 할당 영역 (Runtime)
- Stack → 지역변수, 매개변수 영역 (Compile)
지금까지의 내용을 정리하자면,
- 자료구조: 데이터를 효과적으로 효율적으로 저장하거나 관리하기 위한 방법
- 효율적으로 관리해야 하는 이유: 실제로 프로그램이 실행되는 메모리의 자원은 한정되어 있기 때문에
- 데이터: 무조건 0과 1로 이루어진 이진법으로 저장하며 편의를 위해 16진법으로 표기
- 메모리 구조: Code, Data, Heap, Stack
Stack
`Stack` 자료구조는 쌓는다는 의미를 가지고 있다.
`LIFO(Last In First Out)`, `FILO(First In Last Out)`과 같이 `Stack`은 데이터를 `Push`하고 `Pop`이 한 곳에서 일어난다.
`Stack` 자료구조의 장점은 구현이 간단하고 빠르다는 것이다. History Stack에 아래부터 위로 순차대로 쌓인다.
그러나, 중간 데이터에 접근하기가 어렵다는 것이 단점이다.
Queue
`LILO(Last In Last Out)`, `FIFO(First In First Out)`처럼 마지막에 들어간 것이 마지막에 나오고, 처음에 들어간 것이 처음에 나오는 게 `Queue` 자료구조이다.
`Queue` 구조는 `Rear`와 `Front`로 구성되어 있고, `Enqueue`로 넣고, `Dequeue`로 내보낸다.
쉽게 생각하면, 스타벅스에서 고객이 주문하는 것을 떠올리면 된다. 주문한 고객 순서대로 주문 상품이 전달된다. (가끔 빠르게 제작되는 음료의 경우에는 다음 순서가 더 빨리 받아가는 경우가 존재하지만 이는 예외)
이벤트 루프에서의 Stack, Queue
Queue
브라우저가 동작할 때, 자바스크립트에서 `Memory Heap`, `Call Stack`에 데이터와 함수들이 쌓이는데, 이 과정이 동작하는 과정에서 비동기 작업이 완료된 후 실행될 콜백 함수들이 대기하는 공간이 `Callback Queue`이다.
`Callback Queue`은 `Macro Task Queue`, `Micro Task Queue`, `Aniamtion Frame Queue`로 나뉘며, 보통 `Macro Task Queue`, `Micro Task Queue`을 생각하면 된다. 여기서 `Micro Task Queue`가 `Macro Task Queue`보다 우선순위가 더 높다.
- Macro Task Queue: setTimeout, setInterval, DOM, I/O, Network Request, Event Handler
- Micro Task Queue: Promise callback, Mutation Observer API, await in async func
`Task Queue`, `Micro Task Queue`의 예시를 보자.
console.log("Start");
setTimeout(() => {
console.log("setTimeout Start");
}, 0);
Promise.resolve().then(() => {
console.log("Promise Start");
});
console.log("End");
위의 코드의 결과가 어떨지 고민해보자.
// 결과
Start
End
Promise Start
setTimeout Start
본인이 직접 브라우저의 콘솔에 코드를 직접 작성하고 실행하여 결과를 분석해보는 공부를 해보자.
// 동작 과정
1. Call Stack에 console.log("Start")
2. Start 출력
3. Call Stack에 setTimeout()
4. Web APIs에 setTimeout(), Call Stack에 Promise.then()
5. Macro Task Queue에 setTimeout()
6. Micro Task Queue에 Promise.then()
7. Call Stack에 console.log("End")
8. End 출력
9. Call Stack에 Promise.then()
10. Promise Start 출력
11. Call Stack에 setTimeout()
12. setTimeout Start 출력
`Micro Task Queue`가 `Macro Task Queue`의 특징은,
`Micro Task Queue`은 1번 수행할 때 큐를 모두 비운다. `Macro Task Queue`은 1번 수행할 때 1개의 Task만 비운다.
Stack
`Call Stack`은 프로그램이 실행되며, 함수 호출에 대한 정보를 추적하고 관리하는 역할을 맡고 있다.
function A() {
B();
console.log("A");
}
function B() {
C();
console.log("B");
}
function C() {
console.log("C");
}
A();
위와 같은 코드가 있다고 가정한다면, `Call Stack`에 어떻게 쌓일 지 고민해보자.
1. Call Stack에 A()
2. Call Stack에 B()
3. Call Stack에 C()
4. Call Stack에 console.log("C")
5. Call Stack에 console.log("C") Pop
6. Call Stack에 C() Pop
7. Call Stack에 console.log("B")
8. Call Stack에 console.log("B") Pop
9. Call Stack에 B() Pop
10. Call Stack에 console.log("A")
11. Call Stack에 console.log("A") Pop
12. Call Stack에 A() Pop
그리고, 또 한 가지 알아야 할 것은 `Execution Context(실행 컨텍스트)`이다.
`Call Stack`은 정보를 추적하고 관리하는 역할이라는 것은 위에 설명하였다. `Execution Context`을 단어 그대로 해석하면 `실행 맥락`이다. `맥락`은 사물이 서로 이어져 있는 관계를 뜻한다.
그래서 정확히 무엇일까?
`Execution Context(실행 컨텍스트)`는 자바스크립트 코드가 실행되는 환경으로, 모든 코드가 실행될 때 `Execution Context(실행 컨텍스트)`를 생성하고 이 컨텍스트 안에서 코드가 어떻게 실행될 지 관리한다. 자바스크립트 엔진이 코드를 실행할 때 변수, 함수, 객체 등 정보와 스코프를 관리하는 환경으로, 코드가 실행될 때 필요한 모든 정보를 담고 있는 하나의 박스라고 생각하면 된다.
const 첫번째변수 = "Hello";
function 함수() {
const 첫번째변수 = "World";
console.log(첫번째변수);
}
위와 같은 코드가 있을 때, 어떤 값을 출력해야 할까?
`첫번째변수`가 두 개가 있는데 어떤 것을 보여줘야 할지 자바스크립트는 고민하게 될 것이다. `Execution Context(실행 컨텍스트)`는 `Lexical Scope`의 특징이 있는데, 이를 직역하면 `어휘적 범위`이다.
어휘적? 무슨 말일까?
코드가 잓어된 위치를 기준으로 범위가 결정된다는 의미라고 생각하면 된다. 코드가 작성된 위치나 구조를 기준으로 변수와 함수의 스코프(범위)를 결정하는 것을 말하며, 코드가 작성된 시점에서 그 코드가 어디에 위치해 있는가에 따라 변수를 접근할 수 있는 범위가 결정된다.
아래의 코드를 보고 다시 생각해보자. 어떤 값을 출력하게 될까?
const 첫번째변수 = "Hello";
function 함수() {
const 첫번째변수 = "World";
console.log(첫번째변수);
}
바로 함수 안에 있는 "World"를 출력하게 될 것이다.
`Call Stack`은 `Execution Context Stack`처럼 실행 컨텍스트를 쌓는 것과 같다.
코드 종류
- Global Code: 전역에 존재하는 코드. 정의되어 있는 함수나 클래스의 내부 코드는 포함되지 않음
// 전역변수 범위
let 첫번째전역변수 = "Hello";
let 두번째전역변수 = "World";
// 전역변수 범위
function 함수() {
const 첫번째지역변수 = "Hi";
console.log("함수");
}
Function Code: 함수 내부에 존재하는 코드. 함수 내부에 중첩된 함수나 클래스의 내부 코드는 포함되지 않음
let 첫번째전역변수 = "Hello";
let 두번째전역변수 = "World";
function 첫번째함수() {
const 첫번째지역변수 = "Hi"; // Function Code
function 두번째함수() {
const 두번째지역변수 = "Hello";
console.log("두번째함수");
}
console.log("첫번째함수"); // Function Code
}
자바스크립트 엔진의 동작 방식은 소스코드를 `평가`하고 소스코드를 `실행`한다.
여기서 소스코드를 평가하는 것은 `Execution Context`를 생성하고, 코드의 시작부터 끝까지 확인하는 것이다. 이 때, 변수나 함수의 선언문만 먼저 실행하며 식별자를 `Execution Context`에 저장한다.
var 이름;
이름 = "이름명";
위의 코드에서 `var 이름`처럼 코드 선언문만 실행한다는 것이다. 그래서 `Execution Context`에 `이름 = undefined`로 담게 된다.
그리고 소스코드를 실행하는 것은 선언문을 제외한 나머지 코드를 실행하는 것이다. 위의 코드에서 `이름 = "이름명"`이 해당되며, 이 때, 이름이라는 변수에 값을 할당하고는 있지만 이 식별자가 선언되어 있는지 판단하게 된다. 식별자가 선언되어 있다고 판단이 되면 이 값을 할당한다. 그리고 `Execution Context`에 `이름 = "이름명"`으로 바뀌게 된다.
function 첫번째함수() {
const 지역변수 = "안녕";
두번째함수();
}
function 두번째함수() {
const 지역변수 = "반가워";
}
첫번째함수();
위의 코드는 `Execution Context Stack`에 어떻게 쌓일까?
우선, 함수를 실행하기 전에 `Global Execution Context`가 실행된다. 가장 베이스가 된느 실행 구역으로, 특정 함수 안에서 실행되는 코드가 아니라면 코드는 전역 실행 컨텍스트에서 실행된다.여기에서는 `window 오브젝트`인 전역 컨텍스트를 생성하고 `this`를 `Global Object`로 할당한다.
그리고 나서, `첫번째함수`의 실행 컨텍스트가 쌓이고, 그 다음에 `두번째함수`의 실행 컨텍스트가 쌓인다.
면접을 위해 알아둬야 할 것들
- 자료구조가 무엇인가? 그리고 왜 필요한가?
- Event Loop의 Callback Queue의 종류는 무엇인가? 그리고 Callback Queue가 왜 나누어져 있는가?
- Event Loop의 Execution Context와 Call Stack의 관계가 어떻게 되는가?
- 자바스크립트 엔진의 동작 방식이 무엇인가?
- Lexical Scope, Scope Chain이 무엇인가?
Callback과 Promise
`Callback Function`은 다른 함수가 실행이 끝난 뒤 실행되기를 원하는 함수를 말한다.
function 일반함수(callback) {
callback()
}
function 콜백함수() {
console.log("callback 함수")
}
일반함수(콜백함수)
기본적으로 `callback function`은 위와 같은 모습으로 볼 수 있다. 그렇다면, 조금 더 심화된 예시를 보자.
function getId() {
setTimeout(() => {
console.log("ID")
}, 500)
}
function getEmail() {
setTimeout(() => {
console.log("EMAIL")
}, 100)
}
function getPassword() {
setTimeout(() => {
console.log("PASSWORD")
}, 250)
}
getId()
getEmail()
getPassword()
위와 같은 코드가 있다고 가정하자. 그렇다면, 예상되는 실행 결과가 어떻게 될까?
PASSWORD
ID
위 순서대로 결과가 나타날 것이다. 그런데, 이를 `ID`, `EMAIL`, `PASSWORD` 순서대로 보여주게 하고 싶을 때, 어떻게 수정해야 할까? 이럴 때 바로 `Callback`을 사용해서 처리하면 된다.
function getId(callback) {
setTimeout(() => {
console.log("ID")
callback()
}, 500)
}
function getEmail(callback) {
setTimeout(() => {
console.log("EMAIL")
callback()
}, 100)
}
function getPassword() {
setTimeout(() => {
console.log("PASSWORD")
}, 250)
}
getId(() => getEmail(() => getPassword()))
자, 다시 한 번 더 다른 예시를 보자.
let id, email. password
function getId() {
setTimeout(() => {
id = "1234"
}, 500)
}
function getEmail() {
setTimeout(() => {
email = "test@test.com"
}, 100)
}
function getPassword() {
setTimeout(() => {
password = "qwer1234"
}, 250)
}
function login(id, email, password) {
console.log(id, email, password)
}
getId()
getEmail()
getPassword()
login(id, email, password)
위와 같은 코드가 있을 때, 실행 결과를 예상해보자. 혹시 첫 번째 예시처럼 나타날 거라고 예상을 했을까?
undefined undefined undefined
결과는 `undefined`를 출력한다. 그 이유는 무엇일까? 알고 있듯이 `setTimeout`이 종료되기 전에 `login` 함수를 호출(실행)하고 있다. 즉, 값이 저장되기 전에 `login`함수를 호출하다보니 해당 변수의 값을 알 수 없다고 나타내는 것이다.
그렇다면, 선언한 순서대로 저장되고 그 다음 `login` 함수가 동작되게 하도록 하려면 어떻게 해야 할까?
let id, email. password
function getId(callback) {
setTimeout(() => {
id = "1234"
callback()
}, 500)
}
function getEmail(callback) {
setTimeout(() => {
email = "test@test.com"
callback()
}, 100)
}
function getPassword() {
setTimeout(() => {
password = "qwer1234"
callback()
}, 250)
}
function login(id, email, password) {
console.log(id, email, password)
}
getId(() => getEmail(() => getPassword(() => login(id, email, password))))
두 번째 예시 코드처럼 `callback`을 사용해서 처리하면 원하는 대로 동작하게 만들 수 있다.
그런데, 이런 경우가 많을까? 생각보다 많다.
`유저 정보 API`에서 `userId` 값을 가져오고, 해당 ID 값을 활용해 `무인 주차 서비스 가입 정보 API`의 가입 결과를 가져오려는 과정에서도 아직 ID 값을 전달받지 않은 상태에서 가입 결과에 대한 요청을 하게 되는 등 충분히 발생할 수 있다.
또한, 가입 상태에 따라 다른 페이지로 이동하는 방식도 있을 것이다.
function parkingServicePage() {
const router = useRouter()
getUserData(jwtToken, function(userId: string) {
getJoinedParkingService(jwtToken, userId, function(joined: boolean) {
if(jointed) router.replace('/service/parking/active')
else router.replace('/service/parking/landing')
})
}
}
이보다 많은 조건에 따라 다른 페이지 이동이 더 많아지거나, 버튼, 이벤트 등이 나타나게 할 수 있을 것이다.
그러나, 이 때 주의해야 할 것은 `callback 지옥`에 빠지지 않도록 해야 하는 것이다.
함수1(function (결과1) {
함수2(결과1, function(결과2) {
...
}
})
Promise
`Promise` 객체는 비동기 작업이 맞이할 미래의 완료 또는 실패와 그 결과 값을 나타내는 것이다. `Promise`가 생성된 시점에는 알려지지 않았을 수도 있는 값을 위한 대리자이며, 비동기 연산이 종료된 이후에 결과 값/실패 사유를 처리하기 위한 처리기를 연결할 수 있다.
`Promise` 객체는 말 그대로 `객체(Object)`이다.
// Promise
현재 상태: Pending
Fulfilled → then() → finally()
Rejected → catch()
const 프로미스객체 = new Promise((resolve, reject) => {
const 비동기작업결과 = 비동기함수()
if (비동기작업결과) {
resolve(비동기작업결과)
} else {
reject("비동기 작업 실패")
}
})
위의 `Promise` 예시 코드를 활용해서 `parkingServicePage` 함수에서 구현된 내용을 `Promise`로 수정해서 구현할 수도 있을 것이다.
`Promise`를 `setTimeout`으로 구현해보는 시간도 가져보자.
// Promise Factory Function
function promiseSetTimeout(ms: number) {
return new Promise((resolve) => {
setTimeout(() => {
resolve()
}, ms)
})
}
function main() {
const delay500ms = promiseSetTimeout(500)
delay500ms.then(() => {
console.log("500ms 후 출력")
})
}
그렇다면, `Promise`와 `이벤트 루프`와는 어떤 관계가 있을까?
const promise = Promise.resolve(1)
promise.then((data) => console.log(data))
console.log("코드 실행 완료")
위 코드의 실행 결과를 예상해보자. `Promise`는 비동기 작업으로 위의 내용에서 `비동기 작업`으로 처리되는 과정에 대해 다시 떠올려보면 가장 아래의 `console.log`가 출력될 것이고, 그 다음에 `Promise`의 작업이 마친 결과 값이 출력될 것이다. (여기에서는 1)
Async와 Await
그러나, `Promise`에는 한계가 존재한다.
`성공` 또는 `실패` 상태만 처리할 수 있거나, 추가 구현이 없는 경우에 한 번 실행되면 중간에 취소를 할 수 없다거나, 비동기 작업을 병렬로 수행해 조건에 따라 처리해야 하거나, 내용이 많아질수록 복잡해진다는 것이다.
특히, 경량화가 필요할 때 `Async / Await`을 사용하면 개선할 수 있다. 물론, 이 때 추가 비용이 들 수 있다.
그렇다면, `Async / Await`를 도입한 이유가 `Promise`보다 기능적으로 우수하기 때문일까?
그보다, `Promise` 객체를 더욱 다루기 쉽게 만들기 위해 도입한 것이라고 생각한다. 문법만 다르게 해주는 편의기능이라고 보면 된다. 실제로 자바스크립트는 `Async / Await`을 `Promise` 객체로 처리하고 있다.
Async / Await
- 문법만 다르게 해주는 편의기능
- 템플릿 리터럴
- 구조 분해 할당
- 속성명 단축
- 삼항 조건 연산자
- Syntax Sugar
`await` 키워드는 `async function` 내부에서만 사용이 가능하고, `async function`은 `Promise` 객체를 반환한다. 그리고 `async function`의 `return 1`은 `Rromise resolve(1)`과 동일하다.
이벤트 루프를 고려한 코드
위의 `이벤트 루프`에 대한 내용을 숙지하자.
`setTimeout`이 `Callback Queue`에서 나와 출력되는 것과 `Macro/Micro Task Queue`가 있고, 둘의 우선순위가 어떻게 되는지 그리고 `Micro Task Queue`는 한 번 수행할 때 큐를 모두 비우는데 모두 비워질 때까지 다른 것을 할 수 없다는 것 등을 말이다.
여기에서 내용을 더하자면, `Callback Queue`에는 `Animation Frame Queue`가 있다. 이의 우선 순위는 `Macro`와 `Micro`의 사이에 존재하며, 브라우저 렌더링 역할을 수행한다.
`Macro`와 `Micro`는 `Call Stack`이 비워지면 이벤트 루프에 의해 다시 실행되지만, `Animation Frame Queue`는 렌더링과 함께 동작하여 `Callback`을 다시 `Stack`에 반환하지 않고 렌더링이 준비가 됐을 때 호출하는 것이 차이점이다.
만약, `Micro`에 수 많은 작업이 들어가게 된다면, 'Micro`의 작업이 끝날 때가지 다음 작업을 하지 않기 때문에 `Animation Frames`에 렌더링이 발생하지 않는 문제가 발생할 수 있다. 물론 이벤트가 동작하지 않은 건 아니지만, `Micro`의 모든 작업이 마칠 때까지 렌더링을 대기하고 있게 된다.
동작 과정에 대해 확인해보고 싶다면, 개발자 도구의 콘솔에서 직접 코드를 작성하여 확인해보는 공부도 좋다.
// Micro
for (let i = 0; i < 10**7; i+=1) {
queueMicrotask(() => {})
}
for (let i = 0; i < 10**15; i+=1) {
queueMicrotask(() => {})
}
// Macro
Click Event
포인트
자바스크립트의 함수는 `일급 객체`이다. 그리고 자바스크립트는 `일급 함수`를 가진다.
그 중 `Callback Function`은 다른 함수가 실행이 끝난 뒤 실행되기를 원하는 함수이다.
function 일반함수(callback) {
callback()
}
function 콜백함수() {
console.log("콜백 함수")
}
일반함수(콜백함수) // → 함수의 인자값으로 함수 전달
const 함수가들어간변수 = function() {
console.log("함수가들어간변수")
}
함수가들어간변수()
여기서 `일급 객체(First Class Object)`는 기술적인 것이 아닌 개념적인 것에 가깝다. 이는, 다른 객체들에 일반적으로 적용이 가능한 연산을 모두 지원하는 객체이며, 함수의 매개변수가 될 수 있어야 한다. 그리고 함수의 반환 값이 될 수 있어야 하고, 명령문으로 할당할 수 있어야 하며, 동일하게 비교할 수 있어야 한다.
무슨 말일까? 예시 코드를 보고 조금 더 이해해보자.
// 함수의 매개변수가 될 수 있어야 한다
function callback(fn) {
fn()
}
callback(function() {
console.log("Hello")
})
// 함수의 반환 값이 될 수 있어야 한다
function getCreateDataHandler() {
return function(userId) {
mutateCreate(userId)
}
}
const createDataHandler = getCreateDataHandler();
createDataHandler('1234')
// 명령문으로 할당할 수 있어야 한다
const helloFn = function() {
console.log("Hello")
}
helloFn()
// 동일하게 비교할 수 있어야 한다
const helloFn = function() {
console.log("Hello")
}
if (typeof helloFn === 'function') {
console.log("helloFn는 함수")
}
혹시, 매개변수를 값으로 받을 때, 여러개의 매개변수를 받아서 처리할 수 있을까?
const getInformation = (name) => (phone) => (address) => `${name} ${phone} ${address}`
getInformation('홍길동')('01012345678')('서울')
또한, 함수가 여러 번 호출되어 부분적으로 인수를 받아서 최종적으로 값을 반환하는 패턴으로 `커링(Currying) 함수`로 활용할 수도 있다.
const getInformation = (grade) => (classNum) => (name) => `${grade}학년, ${classNum}반, ${name}`
const getGradeClass = getInformation(2)(1) // grade와 classNum의 부분적으로 인수 적용해 함수 생성
getGradeClass("홍길동") // 마지막 인수까지 받아 최종 문자열 반환. 2학년, 1반, 홍길동
getGradeClass("임꺽정") // 2학년, 1반, 임꺽정
3개의 인수를 받아 최종 문자열을 반환하는 함수로, 한 번에 모든 값을 받지 않고 하나의 인수만 받아 또 다른 함수를 내부적으로 반환하도록 하고 있다. `grade`와 `classNum`을 받아서 처리하고, 그 다음에 다시 `name`을 받아서 함수를 반환하여 마지막 함수는 모든 값을 합친 문자열을 반환하고 있는 코드이다.
면접 단골 질문
ES5에서 ES6
ECMAScript가 정확히 무엇인가?
ES5에서 ES6로 넘어모녀서 변화된 요소의 이유는 무엇인가?
SPA / CSR / SRR
각 어떤 개념을 가지고 있는가?
기술 스택을 선택할 때 어떤 점을 고려해야 하는가?
React 16+ 동작 원리
React 16 버전부터 변화된 가장 큰 점은 무엇인가?
그 변화는 무엇때문에 일어났는가?
그것을 통해 어떤 이점을 얻을 수 있는가?
비동기 처리에서 고려해야 할 점
`JavaScript`의 `Promise.all(Promise.allSettled)`은 `API waterfall`을 방지하는 데 도움이 된다.
예를 들어, 아래의 예시 코드가 있다고 가정하자.
const userId = await getUserData()
const parkingId = await getActiveParking(userId)
const history = await getParkingHistory(userId, parkingId)
trackingEvent(userId, parkingId, history)
위의 코드는 모두 비동기로 각 값을 가져오고 있다. 이는 각 `await`가 순차적으로 실행되어 각 순서의 `await`가 완료된 후에야 다음 `await`가 실행되는 방식으로 모두 완료되기까지 시간이 누적된다. 위 코드는 서로에 대해 의존하고 있다.
아래의 코드 역시, 서로 의존하지 않는다는 점과 다를 뿐, 각 `await`가 순차적으로 실행됨으로써 시간이 누적되는 문제가 있다.
const userId = await getUserData()
const weatherData = await getTodayWeather()
const leftSpace = await getAParkZoneLeftSpace()
const leftEvent = await getEventLeftDay()
trackingEvent(userId, weatherData, leftSpace, leftEvent)
이 문제를 해결하기 위해서 `Promise.all`을 사용할 수 있다. 이는 각 요청을 병렬로 실행하여 요청 시간을 줄일 수 있다.
const [userId, weatherData, leftSpace, leftEvent] = await Promise.all([getUserData(), getTodayWeather(), getAParkZoneLeftSpace(), getEventLeftDay()])
trackingEvent(userId, weatherData, leftSpace, leftEvent)
여기서 `Promise.all()`은 모든 프로미스가 이행되면 이행되고, 프로미스 중 하나라도 거부되면 거부된다. 즉, 하나라도 실패하면 진행할 수 없다.
`Promise.allSettled()`는 모든 프로미스가 해결되면 이행된다. 이는, 실패해도 진행할 수 있다.
Event Loop 정리
`이벤트 루프`는 싱글 스레드로 동작하는 JavaScript를 브라우저에서 동시성을 제공하기 위한 동작 방식을 의미한다.
`JavaScript는 싱글스레드` != `JavaScript Runtime은 싱글스레드`
- Callback Queue: Macro Task Queue / Micro Task Queue / Animation Frame Queue
- 우선순위: Macro Task Queue < Animation Frame Queue < Micro Task Queue
엔지니어가 가져야 하는 습관
- 기술 면접 질문은 왜 할까?
- 기업은 어떤 사람을 원할까?
- 기능 구현보다 중요한 것은 무엇일까?
- 그리고 엔지니어는 무엇에 집중해야 할까?!
`이벤트 루프`는 `동기와 비동기`, `Callback`, `싱글스레드와 멀티스레드`, `Execution Context`, `Async / Await`, `Promise`, `Queue / Stack`에 대한 지식도 갖추고 있어야 한다.
그리고 엔지니어는 추상화된 기능들에 관심을 가져야 한다. 당연히 동작하는 것은 없다는 사실을 인지하고 있어야 한다. 모든 동작들에 대해 당연하게 생각하지 말아야 한다. 어떻게 동작하는지, 왜 그렇게 동작하는지 궁금해하고 알아야 한다.
모든 선택에 이유가 있어야 한다. 단순히 유명하거나 많이 사용한다는 이유만으로는 이유가 되지 않는다. 왜 사용해야 하는지 명확한 이유를 갖춰야 한다.
예를 들어, React를 선택한 이유에 대해 "많이 사용하기 때문"이 아니라 "프로젝트 팀원 구성이 모두 저연차이기 때문에 최대한 러닝커브를 줄이기 위해 사용량이 많고 커뮤니티가 활성화되어 있기 때문이다"라는 명확한 이유가 있어야 한다.
깃허브 코파일럿과 프론트엔드
깃허브 `코파일럿`은 AI 기반 자동 완성을 제공하여 대규모 코드베이스를 학습한 AI가 실시간으로 코드를 제안하며, 다양한 언어를 지원해 프론트엔드 개발에 필요한 HTML, CSS, JavaScript 등 다양한 언어를 지원한다. 그리고 개인화된 경험을 주어 사용자의 코딩 스타일을 학습하여 점점 더 정확한 제안을 제공한다.
프론트엔드 개발의 코파일럿 장점
- 생산성 향상
- 코딩 속도가 크게 향상되어 개발 시간 단축
- 반복적인 작업이 줄어 창의적인 업무 집중 가능
- 최신 트렌드 학습
- 코파일럿을 통해 최신 프론트엔드 개발 패턴과 트렌드를 자연스럽게 습득
- 코드 품질 개선
- AI 제안을 통해 보다 일관되고 효율적인 코드 작성 가능 → 버그 감소
코파일럿으로 개발 환경 최적화 팁
- 프로젝트 구조화
- 명확한 폴더 구조와 일관된 파일 명명 규칙으로 코파일럿 이해도 향상
- 주석 개선
- 상세하고 명확한 주석으로 코파일럿이 더 정확한 코드 제안하도록 유도
- 스니펫 활용
- 자주 사용하는 코드 패턴을 스니펫으로 저장하여 코파일럿과 함께 활용
코파일럿을 이용한 CSS 스타일링(빠른 스타일 적용)
- 기본 스타일 생성
- 코파일럿이 프로젝트에 적합한 초기 CSS 구조와 변수 제안
- 레이아웃 코드 작성
- `Flexbox`나 `Grid` 시스템을 이용한 레이아웃 코드를 빠르게 생성
- 반응형 디자인
- 다양한 화면 크기에 대응하는 미디어 쿼리를 자동으로 제안
JavaScript 기본 구조 작성
- 모듈 패턴
- 코파일럿을 활용해 캡슐화된 모듈 구조 쉽게 작성 가능
- 이벤트 리스너
- 다양한 이벤트에 대한 리스너 코드를 자동으로 생성
- DOM 조작
- 효율적인 DOM 조작 코드를 코파일럿의 제안을 통해 작성 가능
- 코드 최적화
- 성능을 고려한 JavaScript코드 구조화 방법 학습
함수 자동 완성 활용
함수 시그니처를 작성하면 자동으로 파라미터와 반환 값 추론하며, 함수 내부 로직을 자동 생성하며 코드 최적화 팁을 제공한다.
코파일럿은 함수 이름, 파라미터, 반환 값, 함수 내부 로직에 대한 제안을 제공하며 코드 작성 시간 단축 및 코드 품질 향상에 도움을 준다.
주석을 이용한 코드 생성 기법
명확한 의도 전달을 위한 주석 작성법과 복잡한 알고리즘 구현 시 단계별 주석 활용 그리고 주석을 통한 코드 구조 설계 방법을 활용할 수 있다.
코파일럿 다중 제안 기능 활용
다양한 코드 제안 탐색 방법, 상황에 맞는 최적의 제안 선택 기준 그리고 제안된 코드의 커스터마이징 및 개선 방법을 제공한다.
폼 유효성 검사 구현(정규표현식 활용)
일반적인 폼 필드 유효성 검사 패턴을 소개하며, 코파일럿을 이용한 정규표현식 생성 및 테스트가 가능하고, 실시간 유효성 검사 구현 방법을 제공한다.
SEO 최적화(메타 태그 및 시맨틱 마크업)
주요 SEO 메타 태그 설명 및 활용법을 알려주고, 시맨틱 HTML 구조의 중요성과 예시를 보여주며, 코파일럿을 활용한 SEO 친화적 코드 작성을 도와준다.
코파일럿을 이용한 단위 테스트 작성
단위 테스트의 중요성 및 기본 구조를 설명하며, 코파일럿을 활용한 테스트 케이스 생성 방법을 알려준다. 그리고 다양한 테스트 시나리오를 고려할 수 있다.
'여러가지 활동 > 프리온보딩 프론트엔드 챌린지' 카테고리의 다른 글
레거시 유지보수와 최적화 및 데이터 (1) | 2025.01.20 |
---|---|
레거시 코드를 관리하고 유지 보수하는 방법 (1) | 2025.01.13 |
백엔드 개발자의 역할과 이력서 및 포트폴리오 구성 (0) | 2024.08.18 |
프론트엔드의 React와 채용 과정 알아보기 (3) | 2024.08.15 |
객체지향 프로그래밍과 디자인 패턴 (0) | 2024.07.18 |
- Total
- Today
- Yesterday
- 신입개발자가 준비해야 할 것들
- React
- 최종추가합격
- 개발 이력서 지원 팁
- 설명회느낌점
- PostechAppleDeveloperAcademy
- LottieFiles
- javascript
- 개발자이력서꿀팁
- node
- 스프링
- #포스텍애플디벨로퍼아카데미
- 포스텍애플아카데미
- 그룹인터뷰후기
- Default Branch
- 자바스크립트
- 조코딩과함께
- Singleton
- 원티드 프리온보딩 챌린지
- Frontend
- DB Error MongooseServerSelectionError
- 프론트엔드 챌린지
- 코딩테스트 대비
- 원티드 프리온보딩
- 깃허브 Merge
- 포스텍애플디벨로퍼아카데미
- 싱글톤
- if(kakao)dev2022
- Express
- 고민한 부분
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |