ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 자바스크립트(JS)의 동작원리
    CS 2024. 8. 8. 21:52

    자바스크립트는 오늘날 웹 개발에서 가장 널리 사용되는 언어 중 하나로, 다양한 환경에서 실행되며 복잡한 웹 애플리케이션을 구현할 수 있게 도와준다. 그러나 자바스크립트는 싱글 스레드 언어로, 이로 인한 한계가 존재한다.

     

    이러한 한계를 극복하고 웹 애플리케이션을 효율적으로 동작시키기 위해, 자바스크립트는 비동기 처리와 다양한 메커니즘을 도입하였다.

     

    어떠한 언어를 잘 사용하기 위해선, 그 언어 문법에 대한 이해도 필요하지만 어떤 식으로 동작하는지를 알아야 그 언어의 본질을 살려 개발을 할 수 있다. 자바스크립트를 더욱 잘 사용하기 위해 자바스크립트의 자세한 동작원리부터 비동기 처리, 그리고 React와 Vue.js와 같은 프레임워크에서의 비동기 처리 방식을 알아보도록 하자.

     

    ❓ 자바스크립트(JS) ❓

    자바스크립트는 웹페이지에 생동감을 불어넣어주는 프로그래밍 언어이다.

     

    자바스크립트로 작성된 프로그램을 우리는 script라고 하는데, 이 스크립트는 웹페이지의 HTML 안에 작성할 수 있고, 웹페이지를 불러올 때 스크립트가 자동으로 실행된다.

     

    이 자바스크립트 코드를 해석하고 구동하기 위해서는 엔진이 필요한데, 여러 다양한 엔진 중에 가장 많이 사용되는 엔진은 오픈소스로 개발된 Google의 V8 엔진이다. (Node.js도 이 V8 엔진을 사용하여 자바스크립트 런타임 환경을 구성한다.)

     

    이 자바스크립트의 이름에는 재미있는 이유가 담겨 있다.

    이 자바스크립트는 처음 만들어졌을 당시에 'LiveScript'라는 이름을 가졌다.
    그런데 당시에는 자바의 인기가 아주 높은 상황이였기에, '자바의 동생' 격인 언어로 홍보를 하면 도움이 될 것이라는 결정으로 인해 이름이 '자바스크립트'가 되었다.

    자바와 자바스크립트는 아무런 연관이 없고, 오로지 홍보를 위한 목적이었던 것이다.

     

    ❓ 싱글 스레드 언어 ❓

    한 번에 한 가지 작업만 처리할 수 있는 언어이다.

     

    자바스크립트는 싱글 스레드 언어인데, 이는 즉, 하나의 콜 스택(Call Stack)을 사용한다는 의미이다.

     

    이러한 특징으로 인해 자바스크립트는 동기적으로 작동하지만, 웹 애플리케이션에서는 동시에 여러 작업을 수행해야 하는 경우가 많다.

    자바스크립트는 이러한 특성을 보완하기 위해 비동기 매커니즘을 도입하였다.

     

    이 비동기 매커니즘은 다음과 같다.

    • 콜백 함수: 비동기 작업이 완료되면 실행되는 함수
    • 프로미스(Promise): 비동기 작업의 완료 상태를 나타내는 객체
    • async/await: 프로미스(Promise)를 기반으로 한 비동기 코드의 간결한 표현 방법

    비동기 처리의 핵심은 이벤트 루프(Event Loop)콜백 큐(Callback Queue)의 개념이다.

    이에 대한 내용을 자세히 다루기 전에 JS 엔진에 대해서 먼저 짚고 넘어가도록 하자.

     

    ⭐️ JS 엔진의 2가지 구성요소 ⭐️

    JS 엔진은 자바스크립트 코드를 해석하고 실행하는 프로그램이다.

     

    위에서 설명한 것처럼, 자바스크립트 코드를 해석하고 구동하기 위해서는 엔진이 필요하다.

    이 자바스크립트 엔진의 주요 구성요소는 다음과 같다.

     

    메모리 힙(Memory Heap)

    메모리 힙은 메모리 할당과 데이터 저장, 변수, 함수 저장 및 호출 작업이 발생하는 공간이다.

     

    즉, 데이터를 저장하는 창고라고 간단하게 설명할 수 있다.

     

    JS 엔진은 이를 통해서 메모리를 동적으로 관리하는데, 이때 가비지 컬렉션이라는 JS 엔진이 메모리 관리를 자동으로 처리하는 중요한 매커니즘이 존재한다.

     

    이 가비지 컬렉션이 왜 중요한가를 조금 살펴보도록 하자.

    💡 가비지 컬렉션의 중요성 💡
    가비지 컬렉션 덕분에 Javascript 개발자는 메모리 관리를 직접 처리하지 않고도 메모리 누수를 방지할 수 있다.
    하지만, 자동화된 프로세스라고 해도 완벽하지 않아, 메모리 누수를 야기할 수 있는 잘못된 코드 패턴은 피하는 것이 좋다.

    예를 들어, 이벤트 리스너를 설정해 놓고 해제하지 않거나, DOM 요소에 대한 참조를 유지하면서 해당 요소를 삭제하지 않는 등의 경우에는 가비지 컬렉션이 작동하지 않아 메모리 누수를 발생할 수 있다.

    결론적으로 가비지 컬렉션은 메모리 효율성을 높이는데 도움을 주며, 개발자가 복잡한 메모리 할당과 해제 문제를 직접 다루지 않도록 해주는 중요한 매커니즘이지만, 모든 것을 해결해 주는 것이 아니기에 메모리 누수 방지를 위해 신경을 써야 하는 것은 필수적이다.

     

    콜 스택(Call Stack)

    콜 스택은 호출 스택을 순서대로 기록하고 순차적으로 하나씩 처리하는 공간이다.

     

    이곳에서는 실행중인 코드를 트래킹. 즉, 추적 및 감시하는 공간이라고 이해하면 된다.

    콜 스택의 경우, 호출 스택 사이즈보다 많은 요청이 들어오면 무한 루프에 빠지면서 스택 오버플로우라는 에러가 발생하게 된다.

     

    ❓ JS 런타임 ❓

    자바스크립트 코드가 실행되는 환경을 의미한다.

     

    메모리 힙과 호출 스택으로 이루어진 JS 엔진만으로는 웹이 동작할 수 없다.

    엔진과 더불어 Web APIs, Callback Queue, Event Loop로 이루어진 이 자바스크립트 런타임 환경이 웹 동작을 완성하는 것이다.

     

    그럼 이 구성요소에 대해 자세히 알아보도록 하자.

    Web APIs

    브라우저가 제공하는 비동기 기능을 통해 싱글 스레드의 한계를 극복한다.

     

    이때 비동기 기능에는 DOM 이벤트, AJAX 요청, setTimeout 등이 존재하는데, 이 비동기 기능들을 사용하여 자바스크립트가 싱글 스레드임에도 불구하고 비동기 작업을 병렬로 처리할 수 있게 한다.

     

    Callback Queue

    Web APIs에서 완료된 비동기 작업의 콜백 함수들이 대기하는 큐이다.

     

    쉽게 더 풀어서 설명하면, Web APIs에 있는 이벤트가 실행된 후, 자바스크립트에서 실행할 콜백 함수들을 보관하고 있는 곳이다.

    이 Callback Queue는 Event Loop에 의해 관리된다.

     

    기존에 우리가 아는 Queue는 FIFO(First-In-First-Out) 방식으로 동작을 한다.

    하지만 ES6부터는 promise, object.observe 등의 콜백 함수가 저장되는 Micro Task Queue가 도입되며 우선순위가 생기게 되었다.

     

    💡 우선순위 💡
    Micro Task Queue(Job Queue) > Animation Frames > Task Queue(Event Queue)
    Micro Task Queue(Job Queue) Animation Frames Task Queue
    (Event Queue==Macro Task Queue)
    - Promise
    - Async/Await
    - process.nextTick
    - Object.observe
    - MutationObserver
    - requestAnimationFrame - setTimeout()
    - setInterval()
    -setImmediate()

     

    Task Queue에 대표적인 setTimeout 함수가 Queue에 담겨도 Micro Task Queue에 해당하는 함수가 있으면 이벤트 루프는 Micro Task Queue에 해당하는 함수를 먼저 동작하게 되는 것이다.

     

    다음 예시를 보면 이해가 더 잘 될 것이다.

    setTimeout(() => console.log("Task Queue"), 0); // 시간을 0초로 세팅해도 순서는 같다
    console.log("Call Stack");
    Promise.resolve().then(() => console.log ("MicroTask Queue"));
    
    // Print
    /*
    첫 번째 출력: Call Stack
    두 번째 출력: MicroTask Queue
    세 번째 출력: Task Qu
    */

     

    🚨 주의할 점 🚨

    이러한 Micro Task를 사용할 때 주의할 점이 있다.

     

    Promise 같은 경우 Micro Task로 처리되므로 일반 Task Queue에 비해 우선권을 가지며 큐로 들어가게 된다.

    하지만, new Promise 등으로 새로운 프로미스를 생성할 때 실행되는 executor 콜백 자체는 생성과 함께 다른 코드와 마찬가지로 동기 실행이 된다.

     

    물론 내부에 비동기 코드가 있다면 해당 내용은 또 비동기로 실행된다.

    이때 각각의 Task는 시분할이 되지 않고 다음 메시지 처리하기 전에 완전히 처리된다는 점이 문제이다.

    이 말은 즉, 지금 실행되고 있는 함수는 메인 스레드에서 절대 다른 코드에 의해 선점이 되지 않는다는 것이다.

     

    따라서, 하나의 Task 처리가 너무 길어지면 뒤에 기다리는 Task들은 그만큼 늦게 시작된다는 것이다.

    이를 최적화하기 위해서는 각 task 처리를 가능한 짧게 분할하는 것이 중요하다.

     

    Event Loop

    콜 스택이 비어 있으면 콜백 큐에서 대기 중인 작업을 콜 스택으로 옮겨 실행한다.

     

    여기서는 Call Stack(콜 스택)과 Callback queue(콜백 큐)를 주기적으로 감시한다.

     

    Event Loop는 콜 스택이 비어있는 경우, 대기열에서 첫 번째 이벤트(콜백 함수, 이벤트 핸들러 등)를 가져와 콜 스택으로 push(푸시)를 하여 JS 코드가 실행될 수 있도록 돕는 역할을 한다. 이런 반복을 Event Loop에서는 Tick이라고 한다.

     

    ✅ React와 Vue.js의 비동기 처리 방식 ✅

    그렇다면 가장 많이 사용되는 React와 Vue.js에서는 어떻게 비동기를 처리할까?

     

    React의 비동기 처리 방식

    • Concurrent Mode(동시성 모드): 비동기 렌더링을 통해 UI 업데이트를 최적화한다.
    • Suspense: 비동기 작업이 완료될 때까지 렌더링을 지연시킨다.
    • useEffect: 비동기 작업을 처리하는 훅으로, 컴포넌트의 마운트 및 업데이트 시 비동기 작업을 수행한다.
    import React, { useState, useEffect } from 'react';
    
    function MyComponent() {
      const [data, setData] = useState(null);
    
      useEffect(() => {
        async function fetchData() {
          const response = await fetch('https://api.example.com/data');
          const result = await response.json();
          setData(result);
        }
        fetchData();
      }, []); // 빈 배열은 컴포넌트가 마운트될 때만 호출됨
    
      return (
        <div>
          {data ? <div>Data: {data}</div> : <div>Loading...</div>}
        </div>
      );
    }

     

     

    코드에서 useEffect 훅은 비동기 API 호출이 완료된 상태를 업데이트하여 컴포넌트를 다시 렌더링한다.

    React 비동기 작업이 완료되면 데이터를 기반으로 UI 업데이트한다.

     

    Vue.js의 비동기 처리 방식

    • 반응형 시스템(Reactivity System): 데이터 변경을 감지하고, 필요시 비동기적으로 DOM을 업데이트한다.
    • 라이프사이클 훅: mounted, created 등에서 비동기 작업을 수행하며, 완료 시 반응형 시스템이 DOM을 자동으로 업데이트한다.
    <template>
      <div>
        <p v-if="data">Data: {{ data }}</p>
        <p v-else>Loading...</p>
      </div>
    </template>
    
    <script>
    export default {
      data() {
        return {
          data: null,
        };
      },
      async mounted() {
        const response = await fetch('https://api.example.com/data');
        const result = await response.json();
        this.data = result;
      },
    };
    </script>

     

     

    위의 Vue.js 코드에서 mounted 훅은 컴포넌트가 마운트된 비동기 API 호출을 수행하고, 데이터를 가져와 data 속성을 업데이트합니다. Vue 반응형 시스템은 data 속성의 변경을 감지하고, 자동으로 DOM 업데이트합니다.

     

    • nextTick: DOM 업데이트 후 추가 작업을 수행할 수 있게 한다.
    this.message = "Hello Vue";
    this.$nextTick(() => {
      console.log("DOM has been updated");
    });

     

    위 코드는 message 변경된 DOM 업데이트되었음을 보장하고, 이후에 실행될 작업을 정의 있게 해준다.

     

    🔥 React Vue.js 비동기 처리 방식의 차이점 🔥

    React는 비동기 처리에서 Concurrent Mode와 suspense 등을 통해 더 세밀하게 비동기 렌더링을 제어한다. 

    특히 Concurrent Mode는 비동기 작업이 많은 애플리케이션에서 사용자 경험을 향상하는 데 초점을 맞추고 있다.

     

    Vue.js반응형 시스템을 기반으로 비동기 작업을 처리하며, DOM 업데이트 이후 작업을 제어할 수 있다.

    nextTick 메서드를 통해 비동기적으로 DOM 업데이트 이후 작업을 제어할 수 있다.

     

    React와 Vue.js 모두 비동기 작업을 효과적으로 처리하기 위한 메커니즘을 제공하지만, 접근 방식에 차이가 있음을 확인할 수 있다.

     

    🐥 결론 🐥

    자바스크립트는 원래 싱글 스레드로 동작하지만, 비동기 메커니즘과 이벤트 루프를 통해 비동기 작업을 처리하여 웹 애플리케이션에서 효율적으로 작동한다. React와 Vue.js는 이러한 자바스크립트의 비동기 처리 방식을 사용해 DOM 업데이트를 최적화하고, 사용자의 경험을 향상시키는 방식으로 동작한다.

    'CS' 카테고리의 다른 글

    자바스크립트(JS)의 Closure  (0) 2024.08.09
    자바스크립트(JS)의 this  (0) 2024.08.08
    throttle과 debounce  (0) 2024.08.02
    Javascript 이벤트 전파와 이벤트 위임  (0) 2024.08.02
    border vs outline  (0) 2024.07.30
Designed by Tistory.