반응형

브라우저의 내부 구성도

 

JavaScript는 싱글 스레드 언어라는 말을 많이 들어보았을 것이다. 하지만 자바스크립트는 웹 브라우저나 NodeJS와 같은 멀티 스레드 환경에서 실행된다. 싱글스레드인 자바스크립트가 어떻게 멀티 스레드 처럼 비동기적으로 실행되는지 알아보고자 한다.

 

 

0. JS Engine


JavaScript 엔진은 자바스크립트 코드를 해석하고 실행하는 인터프리터이고, 브라우저마다 다른 엔진을 사용한다.

그중에서도 가장 대표적인 엔진으로는 Google의 V8 엔진이다. 가장 많이 사용되는 Chrome과 Node.js에서 상요되는 엔진이기도 하기 때문이다.

  • Chrome : V8
  • FireFox : SpiderMonkey
  • Safari : Webkit
  • Explorer, Edge : Chakra

 

Javascript는 싱글 스레드 언어이다.

"싱글 스레드"라는 말은 자바스크립트 엔진이 단일 호출 스택 = 하나의 Call Stack을 사용한다. 라는 뜻이다.

JavaScript 엔진은 크게 Memory Heap과 Call Stack으로 이루어져 있다.

  • Memory Heap : 메모리 할당이 일어나는 장소
  • Call Stack : 코드가 실행될 경우 하나씩 stack의 형태로 쌓이는 장소

 

그런데, 웹 애플리케이션에서는 네트워크 요청이나 이벤트 처리, 타이머와 같은 작업을 멀티로 처리해야 하는 경우가 많다. 따라서 오래 걸리고 반복적인 작업들은 자바스크립트 엔진이 아닌 브라우저 내부의 멀티 스레드인 Web APIs에서 비동기 + 논블로킹으로 처리된다. 비동기 + 논블로킹(Async + Non blocking)Visit Website는 메인 스레드가 작업을 다른 곳에 요청하여 대신 실행하고, 그 작업이 완료되면 이벤트나 콜백 함수를 받아 결과를 실행하는 방식을 말한다.

 

 

이러한 싱글 스레드인 JavaScript의 작업을 멀티 스레드로 작업을 동시에 처리시키게 하던가, 또는 여러 작업 중 어떤 작업을 우선으로 동작시킬 것인지 결정하는 세심한 컨트롤을 하기 위해 존재하는 것이 바로 이벤트 루프(Event Loop) 이다.

💡 비동기로 동작하는 핵심요소는 자바스크립트 언어가 아니라 브라우저라는 소프트웨어가 가지고 있다.

 

 

 

1. Event Loop : 브라우저 동작을 제어하는 관리자


Event Loop의 역할:

  • 비동기 함수 작업을 Web API에 옮기는 역할
  • 작업이 완료된 콜백을 큐(Queue)에 적재
  • Call Stack이 빈 상태가 되면 자바스크립트 엔진에 적재

따라서 이벤트 루프는 Call Stack에 현재 실행 중인 작업이 있는지 그리고 Callback Queue에 대기 중인 작업이 있는지 반복적으로 확인하는 일종의 무한 루프만을 돌고, (그래서 이벤트 '루프' 이다)

일종의 '작업을 옮기는 역할' 만을 한다. 작업을 처리하는 주체는 자바스크립트 엔진과 웹 API 이다.

 

 

2. Web API


그림의 오른쪽에 있는 Wep API는 JS Engine의 밖에 그려져 있다. 즉, 자바스크립트 엔진이 아니다.

Web API 는 브라우저에서 제공하는 API 로, DOM, Ajax, Timeout 등이 있다. Call Stack에서 실행된 비동기 함수는 Web API를 호출하고, Web API는 콜백함수를 Callback Queue에 밀어 넣는다.

 

 

Web APIs는 타이머, 네트워크 요청, 파일 입출력, 이벤트 처리 등 브라우저에서 제공하는 다양한 API를 포괄하고 있다. Web API는 브라우저에서 멀티 스레드로 구현되어 있다. 그래서 브라우저는 비동기 작업에 대해 메인 스레드를 차단하지 않고 다른 스레드를 사용하여 동시에 처리할수 있는 것이다.

 

 

Web APIs의 대표적인 종류

  • DOM : HTML 문서의 구조와 내용을 표현하고 조작할 수 있는 객체
  • XMLHttpRequest : 서버와 비동기적으로 데이터를 교환할 수 있는 객체 AJAX기술의 핵심
  • Timer API : 일정한 시간 간격으로 함수를 실행하거나 지연시키는 메소드들을 제공
  • Console API : 개발자 도구에서 콘솔 기능을 제공
  • Canvas API : <canvas> 요소를 통해 그래픽을 그리거나 애니메이션을 만들 수 있는 메소드들을 제공
  • Geolocation API :웹 브라우저에서 사용자의 현재 위치 정보를 얻을 수 있는 메소드들을 제공

 

이때 오해하지 말아야 할 것은 모든 Web API들이 비동기로 동작되는 것이 아니다. Web API에는 동기적으로 처리되는 것과 비동기적으로 처리되는 것이 모두 있다. 예를 들어 DOM API나 Console API는 동기적으로 처리되고, XMLHttpRequest나 Timer API는 비동기적으로 처리된다.

 

 

3. Callback Queue


비동기적으로 실행된 콜백함수가 보관 되는 영역이다.

  • Task Queue: setTimeout, setInterval, fetch, addEventListener 와 같이 비동기로 처리되는 함수들의 콜백 함수가 들어가는 큐 (macrotask queue 는 보통 task queue 라고 부른다)
  • Microtask Queue: promise.then, process.nextTick, MutationObserver 와 같이 우선적으로 비동기로 처리되는 함수들의 콜백 함수가 들어가는 큐 (처리 우선순위가 높음)

 

같은 queue 안에 적재되는 콜백이라도 어떠한 비동기 작업이냐에 따라 우선순위가 다른 태스크들이 있을 수 있다.

→ Promise.then 결과가 setTimeout보다 우선 처리되는 것 처럼

ex) 해당 링크에서 [3.2.2. MicroTask Queue 처리 과정]을 참고해보자

 

 

4. JavaScript는 왜 Single Thread를 채택했을까?


  • 웹 브라우저 환경:
    • JavaScript는 원래 웹 브라우저에서 실행되도록 설계된 언어이다. 웹 브라우저는 사용자 인터페이스를 담당하므로, 동시성 문제를 최소화하는 것이 중요하다. 싱글 스레드 모델은 복잡한 동시성 문제를 줄여준다.
    • 사용자 인터페이스와 관련된 동작에서, 두 개 이상의 스레드가 동일한 DOM을 동시에 조작할 경우 충돌이 발생할 수 있다. 싱글 스레드 모델은 이러한 충돌을 방지한다.
  • 언어 설계의 단순성:
    • 싱글 스레드 모델은 다중 스레드와 비교했을 때 구현이 더 단순하고 오류가 적다. 스레드 간의 데이터 공유와 같은 동기화 문제를 고려할 필요가 없으므로, JavaScript의 설계와 구현이 단순해진다.
    • 초창기 웹의 요구사항은 현재와 달리 상대적으로 단순했기 때문에, 복잡한 멀티스레딩보다 단순한 모델이 적합했다.
  • 성능과 안전성:
    • 싱글 스레드 모델은 코드 실행의 예측 가능성을 높여준다. 모든 코드가 순차적으로 실행되므로, 디버깅이 상대적으로 쉬워진다.
    • 멀티스레드 환경에서 발생할 수 있는 데드락, 레이스 컨디션 등의 문제를 방지할 수 있다.

 

JavaScript가 싱글 스레드 모델을 채택한 것은 웹 브라우저 환경의 특성과 초기 언어 설계의 단순성, 그리고 성능과 안전성 등의 이유에서 비롯된다. 싱글 스레드 모델은 동시성 문제를 최소화하고, 안정적이고 예측 가능한 코드를 작성할 수 있게 하며, 비동기 프로그래밍을 통해 이러한 한계를 극복할 수 있는 방법도 제공한다. 이러한 이유들로 인해, JavaScript는 여전히 싱글 스레드 모델을 유지하고 있으며, 개발자들에게 직관적이고 효율적인 프로그래밍 환경을 제공한다.

 

'JavaScript' 카테고리의 다른 글

[JavaScript] Full Calendar 속성 및 사용법  (0) 2025.01.02
반응형

0. 요청사항

  • Header : 월 이동, Toay 날짜 이동 커스텀 버튼
  • 달력에 오늘날짜에 리본 표시하기
  • 달력 이벤트 조회 후, 오늘 날짜 기준으로 가장 가까운 event에 select (select시 해당 날짜에 대한 상세 조회)
  • 날짜 클릭 시, 선택된 날짜를 알 수 있게 표시하기 (하늘색 부분)

1. html

html

<body>
  <div id='calendar'></div>
</body>

2. css

css

/* calendar : resize */
#calendar {
  flex: 1;
  height: 100%;
}
#calendar a {
  color: inherit;
}
#calendar :hover {
  cursor: pointer;
}

/* calendar : toolbar, header, title */
.fc .fc-toolbar.fc-header-toolbar {
  margin: 1em 1.5em;
}
.fc .fc-toolbar-title {
  font-size: 1.5em;
  margin: 0 1.25em 0 2em !important;
  white-space: nowrap;
}
.fc-toolbar-chunk {
  display: flex;
  align-items: center;
}
.fc-toolbar-chunk:nth-child(2) {
  flex: 1;
  justify-content: center;
}
.fc-button {
  width: 38.75px;
  height: 33.78px;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}
.fc-today-button {
  white-space: nowrap;
  width: 43.22px;
  height: 33.78px;
}

/* today ribon */
.fc-daygrid-day.fc-day-today {
  z-index: 1;
}
.fc-daygrid-day.fc-day-today .fc-daygrid-day-top::before {
  content: "Today";
  position: absolute;
  top: 5px;
  left: 5px;
  background: linear-gradient(45deg, #ff5f5f, #ff9e9e);
  color: white;
  font-size: 12px;
  font-weight: bold;
  padding: 0 10px;
  border-radius: 3px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.fc-daygrid-day.fc-day-today .fc-daygrid-day-top {
  position: relative;
}

/* custom event */
.fc-daygrid-day-events {
  text-align:left;
  border-radius:50%;
  padding:0px 7px;
  margin-right:3px;
  font-size:1rem;
}

3. javascript

  • fullCalendar 원하는 위치에 다운로드 후 소스추가

html

<script src='/resources/fullcalendar/lib/main.js'></script>

javascript

const today = new Date(); // 'yyyy-mm-dd' 형태로 Format 필요
const colorMap = { '계약금': '#42bd04',
                   '중도금': '#0e8fcf',
                   '잔금': '#d4aa02'
                 };
var prevInfo, calendarEvent, calendar; // FullCalendar에서 사용할 변수 생성

createCalendar(); // calendar Initialize
get_Date(today); // calendar 최초 조회

function createCalendar() {
  var calendarEl = document.getElementById("calendar");
  calendar = new FullCalendar.Calendar(calendarEl, {
    initialView: "dayGridMonth",
    views: {
      dayGridMonth: {
        titleFormat: function (date) {
          return date.start.year + "년" + (date.start.month + 1) + "월";
        },
      },
    },
    headerToolbar: {
      start: "",
      center: "prevYear prev title next nextYear today",
      end: "",
    },
    customButtons: {
      today: {
        text: "오늘",
        click: function () {
          calendar.today();
        },
      },
    },
    locale: "ko",
    height: "100%",
    showNonCurrentDates: false,
    fixedWeekCount: false,
    windowResizeDelay: 300,
    eventClick: function (info) {
      let dInfo = {
        date: info.event.start,
        dayEl: $(info.el).closest(".fc-daygrid-day").get(0),
        dateStr: info.event.startStr,
      };

      calendar.trigger("dateClick", dInfo);
    },
    events: function (info, successCallback, failureCallback) {
      calendarEvent = successCallback;
    },
    eventContent: function (arg) {
      let eventTitle = arg.event.title;
      let bgC = colorMap[eventTitle.slice(0, 3).trim()] || "#ffffff";

      return {
        html:
        '<div style="background-color: ' +
        bgC +
        '; padding: 2px;"><span class="fc-custom-event"><small class="fas fa-circle mx-1"></small>' +
        eventTitle +
        "</span><div>",
      };
    },
    dateClick: function (info) {
      if (prevInfo != null && prevInfo.dateStr != info.dateStr) {
        prevInfo.dayEl.style.backgroundColor = "";
      }
      info.dayEl.style.backgroundColor = "rgba(183, 224, 255, 0.3)";
      prevInfo = info || "";

      $("#INFO_TXT2").text("선택날짜 : " + info.dateStr);

      getDate(calendar.getDate()); // 여기서 해당 날짜(info.dateStr)의 상세 조회 호출
    },
    select: function (info) {
      let dInfo = {
        date: info.start,
        dayEl: document.querySelector("[data-date='" + info.startStr + "']"),
        dateStr: info.startStr,
      };

      calendar.trigger("dateClick", dInfo);
    },
    windowResize: function (arg) {
      calendar.updateSize();
    },
  });

  calendar.render();
}

// Ajax function 예시
function getDate(param) {
  $.ajax({
    url: '/get_EventData',
    method: 'get',
    data: JSON.stringify(param),
    contentType: 'application/json',
    success: (rtnData) => {
      const evtData = rtnData.Data.map(data => ({
        title: data.TITLE,
        start: data.DATE,
        end: data.DATE
      }));

      calendarEvent(evtData);

      // 조회 후 가까운 Event에 Select
      let closestEvent = evtData.filter(event => event.start >= today)
                                  .sort((a, b) => a.start.localeCompare(b.start))[0] || evtData.sort((a, b) => a.start.localeCompare(b.start))[0];
      if (closestEvent) {
        calendar.select(closestEvent);
      }
      }
    },
    error: (err) => console.error('AJAX 요청 실패:', err)
  });
}

4. 속성

  • FullCalendar - JavaScript Event Calendar
  • initialView:
    • 캘린더가 로드될 때의 초기 모습
    • 사용 가능한 View (ex: 'dayGridWeek', 'timeGridDay', 'listWeek')
  • views
    • 특정 Calendar View에만 적용되는 옵션을 지정할 수 있다.
    • ‘dayGridMonth’ View의 Title을 Format 하기 위해 설정
  • headerToolbar
    • 달력 상단의 버튼과 제목을 정의
    • start, center, end는 기본 위치를 자체 css가 잡아주지만, 위 소스에서는 center에 몰아넣고 css로 새로 적용했다.
    • 기본적으로 ‘title’, ‘prev’, ‘next’, ‘prevYear’, ‘nextYear’, ‘today’ 들을 제공하고있고, 커스텀 가능하다.
  • customButtons
    • headerToolbar/footerToolbar에서 사용할 수 있는 사용자 정의 버튼을 정의한다.
    • 혀용되는 속성으로 text, hint, click, icon, bootstrapFontAwesome이 있다.
  • showNonCurrentDates
    • 월별 보기에서 이전 달이나 다음 달의 날짜를 렌더링할지 여부를 지정
    • false 처리하여 맨 위 사진처럼 회색으로 나타남
  • fixedWeekCount
    • 월별 보기에 표시되는 주 수를 결정
  • eventClick: function
    • 사용자가 이벤트를 클릭하면 발생
    • 위 소스에서는 event클릭 시에도 날짜 클릭(dateClick)과 같은 로직을 하기에 Element를 임의로 만들어 dateClick을 trigger 시켰다.
    • select에서 style.backgroundColor를 바꿔야 하기에 day Element가 필요함.
  • events: function
    • 사용자가 이전/다음을 클릭하거나 뷰를 전환할 때 트리거되며, 해당 영역에서 Ajax 요청을하고, success와 fail에 대한 callback함수를 정의할 수 있다.
    • 위 소스에서는 calendar 함수 밖에서 ajax요청을 보내야 하기 때문에 calendarEvent를 전역변수로 만들어 successCallback 함수를 담아 처리했다.
  • eventContent: function
    • Custom 이벤트를 다양한 위치에서 FullCalendar DOM에 주입시킨다.
    • 위 코드에선 Ajax 요청시 successCallback 함수를 가르키는 calendarEvent를 실행하여 return Data를 담아 발생시켰다.
    • evtData를 colorMap에 맞춰 이벤트 DIV를 DOM에 주입시켰다.
  • dateClick: function
    • 사용자가 날짜나 시간을 클릭하면 발생한다.
    • select한 날짜의 Element를 Full Calendar가 처리가능한 객체로 만들고, 클릭한 날짜의 배경색을 처리한 부분
  • select: function
    • 날짜/시간 선택 시 트리거
  • windowResize: function
    • 브라우저 창 크기가 조정되어 달력의 크기가 변경된 후에 발생하며 windowResize가 트리거되면 달력이 자동으로 새로운 크기에 맞게 조정
    • DOM LOAD가 느려 제대로 동작하지 않을경우 windowResizeDelay 설정이 필요하다.
반응형
반응형

+ Recent posts