반응형

사내 배포용으로 라이브러리를 개발 중인데 처음 보는 이상한 오류가 떴다.

 

module exports에 관한 오류

모노레포 storybook-docs 레포에서

빌드된 tttable(임시 작명이라 가명임;;) 레포의 index.js를 required 하면 오류가 나는것 

평소에 commonjs와 esm, amd 등이 뭔지 헷갈려서 이참에 정리겸 간단하게 요약해본다.

 

CommonJS(CJS)란?

// 모듈 정의
const circle = require('./circle.js');

const radius = 5;
console.log(`반지름 ${radius}인 원의 넓이: ${circle.area(radius)}`);
console.log(`반지름 ${radius}인 원의 둘레: ${circle.circumference(radius)}`);

 

JavaScript를 위한 모듈 로딩 시스템 표준 중 하나이다.

Node.js와 같은 서버 측 JavaScript 환경에서 사용되며, 모듈을 정의하고 로드하기 위한 표준화된 방법을 제공한다. 

node.js나 프론트엔드의 config 파일에서 많이 본 require를 사용한 방식이 commonJS 방식이다.

 

초기 Javascript는 모듈 시스템이 없었기 때문에 모듈화를 위해 commonJS,  AMD 등이 개발되어 사용되었다.

commonJS는 동기적으로 모듈을 로드하기 위해 사용되고,

비동기를 따로 다루기 위해 AMD(Asynchoronous Module Definition)이 비동기적으로 모듈을 다루는 것에 대한 표준안으로 제시되었다고 한다.

 

ECMAScript Module(ESM)이란? 

// 모듈 불러오기
import circle from './circle.mjs';

// 모듈 사용
const radius = 5;
console.log(`반지름 ${radius}인 원의 넓이: ${circle.area(radius)}`);
console.log(`반지름 ${radius}인 원의 둘레: ${circle.circumference(radius)}`);

 

 ES6부터 표준화 된 모듈 시스템으로, 클라이언트에서 흔히 사용하는 import, export를 사용하는 방식이다.

브라우저에서 사용하기 위해서는 type="module"을 script 혹은 package.json에 명시해주면 된다.

 

 

토스 블로그의 CommonJS와 ESM 에 대한 글에 따르면, CJS 모듈은 동기적으로 작동하고 ESM Module은

비동기적으로 작동한다.

 

그래서 ESM에서 CJS를 import 가능하지만 그 역은 안되는데 그 이유는 CJS는 top-level await를 지원하지 않기 때문이며

그 외에도 여러가지 다른 이유가 있어서 서로 호환하기가 쉽지 않다고 한다. (자세한건 링크 참고)

 

즉, 라이브러리를 배포하려면 CJS와 ESM 버젼을 둘다 지원해야 하는것!

 

 

지원하는 방법은 위 코드처럼 package.json에 exports를 두가지 버젼으로 명시해주면 된다.

 

주의할 점은 package.json에 type:"module"가 명시되어 있으면 ESM으로 인정되며 main이 index.js(ESM)로 인식되고, module이 index.cjs(CJS)로 인식될 수 있다.

반대로 type:"module"가  없으면 main이 index.js(CJS)이고 module이 index.mjs로 인식될 수 있기 때문에

exports를 통해 버젼별로 선언해줘야 한다.

 

캡처 찍고 보니 types 설정을 빼먹은거 같은데 내일 넣어야겠다.

(type도 index.d.mts, index.d.cts로 명명된다 함) 

 

 

옛날에 types: module이 정확히 왜 필요한건지 몰랐는데 이번에 알게되어 

의미 깊은 하루였다.

 

reference 

 

https://devblog.kakaostyle.com/ko/2022-04-09-1-esm-problem/

 

ESM 삽질기

저희는 주기적으로 Node.js 모듈을 최신 버전으로 업데이트하고 있습니다. Node.js를 10년째 사용 중인데, CoffeeScript → TypeScript, 콜백 → Async.js → Promise(& async, await) 전환 하면서 몇 번 혼란의 시기가

devblog.kakaostyle.com

 

https://toss.tech/article/commonjs-esm-exports-field

 

CommonJS와 ESM에 모두 대응하는 라이브러리 개발하기: exports field

Node.js에는 두 가지 Module System이 존재합니다. 토스 프론트엔드 챕터에서 운영하는 100개가 넘는 라이브러리들은 그것에 어떻게 대응하고 있을까요?

toss.tech

 

https://velog.io/@yesbb/%EB%AA%A8%EB%93%88-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9D%98-%EC%97%AD%EC%82%AC-%EA%B7%B8%EB%A6%AC%EA%B3%A0-ESM

 

반응형

'Front-end > JavaScript' 카테고리의 다른 글

pnpm 도입기 (feat: 모노 레포)  (0) 2023.07.15
자바스크립트의 async/await에 대해서  (0) 2022.03.31
JS로 Single Page Application 구현  (0) 2022.03.12
Generator  (0) 2022.02.02
호이스팅과 var, let에 대하여  (0) 2022.01.29
반응형

요즘 모노레포를 사용하기 위해서 turbo repo를 사용하면서 궁합이 좋은 패키지 매니저인 pnpm을 사용해보고 있다.

몇주간 사용하며 느낀 점들을 간단히 공유한다.

 

pnpm을 선택한 이유 

출처 : https://toss.tech/article/node-modules-and-yarn-berry

 

npm과 Yarn은 복잡하게 얽힌 dependency들을 단일 루트 하에 위치시키며(Hoisting) 

라이브러리의 packages.json에 명시된 dependencies들을 몰래 최상위로 호이스팅 시킨다.

그 과정에서 packages.json에 명시되지 않은 라이브러리를 사용 가능한데 이를 유령 의존성(phantom dependency)이라 부른다.

 

실제로 turborepo에서 yarn을 사용해본 결과 jest를 모노레포 A에서만 깔았는데 모노레포 B에서도 사용할 수 있었다.

 

사실 jest는 별 문제가 되지 않지만 만약 실제 사용하는 공통 라이브러리들이 유령 의존성으로 꼬이는 위험한 문제가 있을 수 있다고 판단했다. (실제 일하는곳도 yarn을 쓰는데 비슷한 경우로 가끔 고통받았다..; )

 

이를 해결하기 위한 yarn berry와 pnpm을 조사해보았는데, yarn berry는 git에 지속적인 과부하를 주고, pnpm 대비 사용하기 까다롭겠다는 생각에 결국은 pnpm을 선택했다.

 

pnpm의 장점

모노레포는 모든 라이브러리를 전역에 직접 설치하고 호이스팅하는 대신 

전역 저장소(Virtual Store)에 패키지를 공유하고 symlink로 패키징을 참조하는 방식을 사용한다.

 

 

모노레포를 사용하면서 비슷한 라이브러리를 많이 다운받을텐데  중복된 패키지를 설치하지 않아 저장 공간과 네트워크를 절약할 수 있으며, 파일 복사를 최소화하여 더 빠르게 패키지를 설치할 수 있게 되었다.

 

이 기능을 온전히 사용하기 위해서는 루트 디렉토리의 .npmrc에 

node-linker=isolated

node-linker=isolated라는 옵션을 사용해줘야한다. (기본 default가 isolated이다.)

 

가끔 turbo repo의 quickstart repo에 'node-linker: hoisted'라고 명시되어 있는데 조심해야한다.

hoisted라면 npm, yarn처럼 모든 패키지가 호이스팅되게 동작한다.

 

만약 특정 라이브러리만 호이스팅하고 싶다면? 

아까 예시로 든 jest를 생각해보자. 모든 모노레포에 jest를 깔고 버전을 맞춰준다고 생각해보면 정말 고통일 것 같다.

Jest 관련 레포를 만들고 해당 레포를 이용하여 jest 라이브러리 버전을 공유하고 싶지 않을까?

또한 eslint, prettier도 매번 깔아야할까..?

 

이를 방지하기 위해서 pnpm은 public-hoist-pattern라는 옵션을 .npmrc에 제공한다.

 

public-hoist-pattern

  • Default: ['*eslint*', '*prettier*']
  • Type: string[]
public-hoist-pattern[]=*plugin*

(https://pnpm.io/npmrc 에서 발췌)

 

기본적으로 eslint, prettier는 pnpm에서 호이스팅되게 설정되어 있다. 

만약 jest 관련 설정도 호이스팅하고 싶으면 

public-hoist-pattern[]=*jest*

.npmrc에 사용하면 된다.

 

 

reference 

 

 https://toss.tech/article/node-modules-and-yarn-berry

반응형

'Front-end > JavaScript' 카테고리의 다른 글

commonjs와 ESM의 차이  (0) 2023.07.25
자바스크립트의 async/await에 대해서  (0) 2022.03.31
JS로 Single Page Application 구현  (0) 2022.03.12
Generator  (0) 2022.02.02
호이스팅과 var, let에 대하여  (0) 2022.01.29
반응형

*틀린 부분 있으면 피드백 주시면 감사하겠습니다~

 

async/await를 쓸줄은 아는데 정확히 어떻게 동작하는지 애매하게 알고 있어서 작성해봅니다.

 

동작 원리

 

const a = () => {
  console.log("a 시작");
  b();
  console.log("a 끝");
};

const b = async () => {
  console.log("b 시작");
  await c();
  console.log("b 끝");
};

const c = async () => {
  console.log("c 시작");
  await d();
  console.log("c 끝");
};

const d = () => {
  console.log("d")
};

a();

출처 : https://velog.io/@jjunyjjuny/JavaScript-asyncawait%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%8F%99%EC%9E%91%ED%95%A0%EA%B9%8C

 

이 코드를 이해하려면 일단 다음 부분에 대한 이해가 있어야한다.

 

- 자바스크립트의 비동기 처리방식

- 이벤트 루프와 테스크 큐, 마이크로 테스크 큐

- promise(ES6)와 async/await(ES8)

- async 함수는 항상 (명시적, 암묵적 방식으로) promise를 반환한다

 

자바스크립트는 비동기 함수를 처리하기 위해서 이벤트루프라는 방식을 사용하는데 이벤트 루프는 지금 콜스텍이 비었는지(실행하고 있는 task가 없는지)와 테스크 큐(들)에 지금 실행 가능한 task가 있는지를 반복적으로 확인한다. 그래서 비동기 함수는 큐에서 대기하다가 실행이 가능해지면 꺼내져와서 실행하는데, promise같은 경우에는 실행 우선 순위가 매우 높아서(마이크로 테스크큐) 실행 가능해지면 먼저 실행되게 된다.

 

async/await 함수의 동작 순서

 

1. await 키워드가 붙은 대상을 실행한다.

 

2. 1번 대상이 함수가 아니거나, 함수의 실행이 끝나면 '1번 대상을 실행한' async 함수를 일시정지하고 콜스텍에서 마이크로 테스크 큐에 옮긴다. 이때 await를 실행한 위치를 기억한다.

 

3. 지금 실행하고 있던 함수가 콜스텍에서 빠져나왔으니 다른 콜스텍을 실행한다. 

 

4. 콜스텍이 비었으면 2번에서 빠져 나왔던 async 함수를 다시 콜스택으로 옮겨 실행한다. 이때 await가 실행된 위치 다음부터 실행된다. 

 

5. 함수가 끝날때까지 반복...!

 

 

그래서 아까 문제를 다시 풀어보자면

 

1. a를 실행하고 콜스텍에 쌓인다 [a]

 

2. a 시작 출력 

 

3. b를 실행하고 콜스텍에 쌓인다 [a, b]

 

4. b 시작 출력

 

5. await keyword를 만났고 함수이므로 C를 실행하고 콜스텍에 쌓인다.  [a, b, c]

 

6. C 시작 출력

 

7.  await keyword를 만났고 함수이므로 D를 실행하고 콜스텍에 쌓인다.  [a, b, c, d]

 

8. D 출력

및 종료하고 콜스텍에서 빠져나온다. [a, b, c]

 

9. 7번에서 D를 시작했고 실행이 끝났으니 C는 마이크로 테스크큐에 들어가게 된다.  [a, b],  micro = [c]

 

10. 5번에서 C를 시작했고 9번에서 C가 일시정지 되었으므로 B도 마이크로 테스크 큐에 들어가게 된다.  [a],  micro = [c, b]

 

11. 콜스텍에서 B가 빠져나갔으므로 a가 이후 흐름을 실행한다.

a 끝 출력

a가 끝나서 콜스텍에서 빠져나온다. [], micro = [c, b]

 

12. 콜스텍에 비었으므로 이벤트루프가 마이크로 테스트큐에 있는 함수를 실행한다.

 

13. C 끝 출력 및 C가 콜스텍에서 빠져나온다. [], micro = [b]

 

14. B 끝 출력 및 b가 콜스텍에서 빠져나온다. [], micro = []

 

답 : 

a 시작
b 시작
c 시작
d
a 끝
c 끝
b 끝

 

결론 : 

async/await를 쓰면 가독성이 좋은 like-동기 방식으로 실행하는거처럼 보이지만 사실은 엄청 복잡한 비동기 방식으로 실행된다는것을 알게 되었다.  

 

 내부적 원리 요약

 

그래서 마이크로 테스크큐에 넣고 다시 콜스텍에서 실행될때 돌아올 위치를 어떻게 결정하는데? 의문이 떠올랐는데 Generator와 Promise의 원리를 섞어서 처리했다고 한다.

 

참고한 글) https://medium.com/sjk5766/async-await-%EC%9B%90%EB%A6%AC-cc643f18526d

async function fun() {
  await getDrink();
  await haveMeal();
  await drinkSoju();
}

예를 들어 위와 같은 코드가 있다면

 

function* fun() {
  yield getDrink(); // 1
  yield haveMeal(); // 2
  yield drinkSoju();// 3
}

위와 같은 함수로 변경되고 

yield를 만나 실행되고

내부에서 Promise 실행을 한 뒤 

Promise가 끝난다면 내부에서 resolve를 실행하고

Generator의 next()(step 함수 자체구현)를 호출해서 체인을 걸어주는식으로 동작한다고 한다. 

 

그런데 3년 전 글이라 실제로 바벨 홈페이지에 들어가서 돌려보니 (개념상으로는 비슷한데) 약간 달라진점이 있었다.

function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
  try {
    var info = gen[key](arg);
    var value = info.value;
  } catch (error) {
    reject(error);
    return;
  }
  if (info.done) {
    resolve(value);
  } else {
    Promise.resolve(value).then(_next, _throw);
  }
}

function _asyncToGenerator(fn) {
  return function () {
    var self = this,
      args = arguments;
    return new Promise(function (resolve, reject) {
      var gen = fn.apply(self, args);
      function _next(value) {
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'next', value);
      }
      function _throw(err) {
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'throw', err);
      }
      _next(undefined);
    });
  };
}

바벨을 돌려봤더니 위 코드를 보면 

 

step() 부분이 변경되어서

 

asyncGeneratorStep 안에서 만약 Generator가 끝난다면 (예를들어 코드 맨끝줄 상황) 그대로 resolve하는것은 동일한데 

아니라면 Promise의 then (Promise.resolve(value).then(_next, _throw);)을 사용해 첫번째 await 함수까지 실행하고 다음 체인은 마이크로 테스크큐에 넣어주는것으로 구현한것으로 보인다.

 

직접 해보고싶은 분들은 아래 주소로..?

 

https://babeljs.io/repl#?browsers=&build=&builtIns=false&corejs=3.6&spec=false&loose=false&code_lz=FAMwrgdgxgLglgewgAhgQwM4GsCMAKASgG9gBIKJDBAGwFMA6ahAczwHIZbsc2CBuZMmABfYKEixEKdNgBMhZCUEAnWjDDKUEWgHdkABWUIAtnAy08eVRgLIAvAD5ggl67fvkCx8nMwAKnDGtAhgMFZceBQQVHSMLOwyWLK8BAA0yDgADNkE_CJiwJgAntDI4NDwSMgARgpKLlExDEysbNU8ea5oOmhwMKiYuIR8zg2UNM3xbcmdyKTdvf2J8rPk47Et7NUAzLwjosCNE3GtGOjKMHvAtQSH65OttBAAJgD8V8BAA&debug=false&forceAllTransforms=false&shippedProposals=false&circleciRepo=&evaluate=false&fileSize=false&timeTravel=false&sourceType=module&lineWrap=true&presets=env&prettier=false&targets=&version=7.17.8&externalPlugins=&assumptions=%7B%7D 

 

Babel · The compiler for next generation JavaScript

The compiler for next generation JavaScript

babeljs.io

 

 

P.S) 바벨로 트렌스파일 말고 진짜 Generator로 변환하는가 찾아봤는데 V8 주석의 경우 변환한다고 적혀있긴 하다..

 

// ES#abstract-ops-async-function-await
// AsyncFunctionAwait ( value )
// Shared logic for the core of await. The parser desugars
//   await value
// into
//   yield AsyncFunctionAwait{Caught,Uncaught}(.generator_object, value)
// The 'value' parameter is the value; the .generator_object stands in
// for the asyncContext.

주석 발췌 from  https://github.com/v8/v8/blob/17a99fec258bcc07ea9fc5e4fabcce259751db03/src/builtins/builtins-async-function-gen.cc#L247-L254

 

reference

 

https://velog.io/@jjunyjjuny/JavaScript-asyncawait%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%8F%99%EC%9E%91%ED%95%A0%EA%B9%8C

 

[ JavaScript ] async/await는 어떻게 동작할까

await가 만드는 동기처럼 보이는 비동기 코드

velog.io

 

https://medium.com/sjk5766/async-await-%EC%9B%90%EB%A6%AC-cc643f18526d

 

async-await 원리

async-await 을 처음 봤을 때, 기존 call-back 구조와 비교해 소스를 가독성 좋고 간단하게 짤 수 있구나에 감탄했고 어떻게 이렇게 될까 라는 생각이 들었습니다. async-await은 내부적으로 Generator와 Promi

medium.com

 

 

반응형

'Front-end > JavaScript' 카테고리의 다른 글

commonjs와 ESM의 차이  (0) 2023.07.25
pnpm 도입기 (feat: 모노 레포)  (0) 2023.07.15
JS로 Single Page Application 구현  (0) 2022.03.12
Generator  (0) 2022.02.02
호이스팅과 var, let에 대하여  (0) 2022.01.29
반응형

SPA(Single Page Application)이란? 

 

과거에는 웹 페이지를 클릭하면 하나의 문서를 전달하고, <a href="페이지 주소"> 같은 하이퍼 링크를 누르면

새로운 페이지를 서버에서 전송받아 보여주고는 했습니다.

즉, 새로운 페이지를 요청할때마다 정적 리소스가 다운로드 되고 (새로운) 전체 페이지가 전부 다 렌더링 됩니다.

 

SPA와 기존 방식(MPA)

SPA는 기존 방식과 달리 웹 페이지에 필요한 리소스(HTML,CSS,JS)를 한번에 다 다운로드 받은 뒤

새로운 페이지 요청이 있을 경우 페이지 갱신에 필요한 데이터만 서버에서 전달받아 페이지를 갱신하는 방식(AJAX)으로 구현이 가능합니다.

 

즉, 빈 도화지(HTML)에 그림(JS로 동적 렌더링)을 그리는 식으로 작동합니다.


JS로 간단하게 구현해봅시다.

 

폴더 구조

└─src
|   ├─pages
|   |   ├─ RouterButton.js
|   |   ├─ A.js
|   |   ├─ B.js
|   |   └─ C.js
|   ├─BaseComponent.js 
|   └─App.js
└─ index.html

page는 A,B,C 페이지가 있고

요청에 따라 index.html에 동적으로 A,B,C 페이지를 각각 그려줄 것입니다.

하얀 도화지에 그림(렌더링)을 그린다고 보시면 이해가 편합니다.

 

HTML

 

<html>
  <head>
    <title>SPA</title>
  </head>
  <body>
    <main class="app">
    </main>
    <script type="module" src="./src/app.js"></script>
  </body>
</html>

우선 기본 base html입니다. 

우리는 기본 html에 JS로 동적으로 화면을 그려줄 것 입니다.

 

(*type="module"을 쓰면 global scope가 아니고 독립적인 module scope가 생기고, ES6 module(import, export)를 사용 가능합니다.) 

 

간단히 렌더링 되기 이전까지의 과정을 요약해보자면

 

1. www.홈페이지 요청

2. 서버에서 리소스(HTML, JS, CSS)등을 클라이언트에 보내줌

3. HTML,CSS, JS 다운로드 완료

4. JS 동작 

5. HTML에 렌더링

 

BaseComponent.js

 

A,B,C 페이지 javascript폴더에 공통적으로 쓸 부모 클래스를 정의해줍니다.

 

아래에서 쓰이는 예시는 템플릿 메소드 패턴이라고 디자인 패턴을 사용한 것인데

부모 클래스에서 기본적인 로직을 짜고 아래 부분(자식 클래스)에서 구체화하는식으로 동작하게 구현하는 

방식입니다.

 

(예시는 객체지향 패러다임을 썼지만 객체지향적인 컨셉으로 짜도 되고 함수 컨셉으로 짜도 됩니다.)

export default class BaseComponent {

    constructor({$parent, setState, state}){
        this.$parent = $parent;
        this.state = state;
        this.setState = setState;
    }
    
    template(){
        return '';
    }

    #render(){
        const template = this.template();
        this.$parent.insertAdjacentHTML('beforeend', template);
    }

    setEvent(){

    }

    renderSequence(state){
        this.state = state;
        this.#render();
        this.setEvent();
    }
}

constructor에서 주입받는건 3가지인데 

이 중 $parent는 HTML의  <main class="app"></main> 값을 가지고 있는 변수(querySelector)입니다.

setState, state 두가지는 이 후에 차차 알아봅시다.

 

저런식으로 구현한다면 만약 class="app"을 가진 root element가 아니라

다른 element값을 주입해서 그 아래에 렌더링되는식으로 부모클래스가 자식 클래스를 제어할 수 있는 유연성을 가질 수 있습니다.

 

이것을 유식한 말로 DI(Dependency Injection)이라 합니다.

 

상속받은 자식 클래스들 (A.js, B.js, C.js)

 

우리가 자식 클래스에서 구현할것은 

template() 함수 와 setEvent() 함수입니다.

 

template() 함수는 안에 넣을 뼈대(string)를,

setEvent()는 렌더링 이후 페이지에 붙여줄 이벤트만 작성해주면 됩니다. (간단 예시니 event는 붙이지 않았습니다)

 

그럼 부모 클래스가 로직을 알아서 제어해서 렌더링 시켜줄 것입니다.

 

>> 자식 클래스들 코드 

더보기
import BaseComponent from '../BaseComponent.js';

export default class A extends BaseComponent{

    template(){
        return `IM A!
        ${RouterButton()}
        `;
    }

    setEvent(){

    }
}

export default class B extends BaseComponent{


    template(){
        return `IM B!
        ${RouterButton()}
        `;
    }


    setEvent(){
    }
}
export default class B extends BaseComponent{



    template(){
        return `IM B!
        ${RouterButton()}
        `;
    }


    setEvent(){
    }
}

export default function RouterButton(){
    return `
        <button type="button" class="A">
            A
        </button>
        <button type="button" class="B">
            B
        </button>
        <button type="button" class="C">
            C
        </button>
    `
};

 

이제 SpaRouter를 작성해 봅시다.

 

import A from './pages/A.js';
import B from './pages/B.js';
import C from './pages/C.js';

class app{

    constructor(){
        this.state = { 'locate' : window.location.pathname};
        this.root = document.querySelector('.app');
        
        const ObjectForDI = {$parent:this.root, setState : this.setState.bind(this), state : this.state};

        this.A = new A(ObjectForDI);
        this.B = new B(ObjectForDI);
        this.C = new C(ObjectForDI);

        this.render();
        this.setDummyEvent();
    }

    setState (newState){
        const { locate } = this.state;
        this.state = {...this.state, ...newState};
        this.render();
    }
    
    render(){
        this.root.innerHTML = '';

        let { locate } = this.state;

        console.log(locate);

        if(locate === '/A'){
            this.A.renderSequence(this.state);
        } 
        else if(locate === '/B'){
            this.B.renderSequence(this.state);
        }
        else{
            this.C.renderSequence(this.state);
        }

        this.historyRouterPush(locate);
    }

    historyRouterPush(locate) {
        window.history.pushState({}, locate, window.location.origin + locate);
    }
}

new app();

 

코드를 나눠서 살펴 봅시다.

 

라우팅 처리 방식 

 

    render(){
        this.root.innerHTML = '';

        let { locate } = this.state;

        console.log(locate);

        if(locate === '/A'){
            this.A.renderSequence(this.state);
        } 
        else if(locate === '/B'){
            this.B.renderSequence(this.state);
        }
        else{
            this.C.renderSequence(this.state);
        }
    }

지금 페이지의 경로(window.location.pathname) 에 따라 A, B, C 페이지가 라우팅됩니다.

경로가 /A면 A 페이지, /B면 B 페이지, 그외엔 C 페이지가 렌더링 되고 있습니다.

 

 

경로 변경 방식

 

    setState (newState){
        // newState = {locate : 'newPath' }
        this.state = {...this.state, ...newState};
        this.render();
    }

 

 특정 이벤트가 일어나면 setState를 호출하고, 다시 렌더링되게(위에서 보았던 render 함수 호출)됩니다.

 

그리고 state와 setState는 아까 BaseComponent.js의 생성자에서 본 state와 setState의 정체입니다.

부모 클래스(app.js)에서 자식 클래스(BaseCompoent 상속받은 A.js, B.js 등)에게 함수와 공통 객체를 주입해주고,

페이지를 바꿀 (정보 갱신) 타이밍이 오면 setState 함수를 자식 클래스에서 호출해서 해당 페이지를 전부 rerender 하게 됩니다.

 

리엑트에서 상태 변경때 쓰는 방식과 비슷하지 않나요?

내부 구현은 매우 다를거라 예상되지만... 비슷하게 작동합니다.

(이 코드에서 엄밀히 따지면 setState보다는 setRouterPath 등 다른 이름이 어울릴꺼 같긴 해요) 

 

뒤로가기, 앞으로 가기 구현

 

    historyRouterPush(locate) {
        window.history.pushState({}, locate, window.location.origin + locate);
    }

브라우저 API인 히스토리 API를 사용해서 매우 쉽게 구현 가능합니다.

 

이상 간단하게 SPA 구현 방식을 알아봤습니다.

 

 

ref

 

https://devopedia.org/single-page-application

 

Single Page Application

A web application broadly consists of two things: data (content) and control (structure, styling, behaviour). In traditional applications, these are spread across multiple pages of HTML, CSS and JS files. Each page is served in HTML with links to suitable

devopedia.org

 

참고)

 

클릭 이벤트 처리방법

 

    dummyClickEvent = ({target}) => {
        if(target.classList.contains('A')){
            this.setState({...this.state, locate : '/A'});
        }
        if(target.classList.contains('B')){
            this.setState({...this.state, locate: '/B'});
        }
        if(target.classList.contains('C')){
            this.setState({...this.state, locate: '/C'});
        }
    }


    setDummyEvent(){
        this.root.addEventListener('click', this.dummyClickEvent);
    }

 

반응형
반응형

ES6에서 도입된 제너레이터(Generator) 함수는 이터러블을 생성하는 함수이다.

제너레이터 함수를 사용하면 이터레이션 프로토콜을 준수해 이터러블을 생성하는 방식보다 간편하게 이터러블을 구현할 수 있다.

기존 Symbol iterator 로 피보나치 수열 구현

function fibonacci(maxi) {

  let [first, second] = [0,1];

  return {
    [Symbol.iterator](){return this;},
    next() {
      [first, second] = [second, first+second];
      return {value: first, done : first>=maxi};
    }
  }
}

const Fibo = fibonacci(1000);

while(true){
    const {value, done} = Fibo.next();
    if(done) 
        break
    console.log(value);
}
/* console 결과
VM242:20 1
VM242:20 1
VM242:20 2
VM242:20 3
VM242:20 5
VM242:20 8
VM242:20 13
VM242:20 21
VM242:20 34
VM242:20 55
VM242:20 89
VM242:20 144
VM242:20 233
VM242:20 377
VM242:20 610
VM242:20 987
*/

이터레이션 프로토콜)

 

Iteration & for...of | PoiemaWeb

ES6에서 도입된 이터레이션 프로토콜(iteration protocol)은 데이터 컬렉션을 순회하기 위한 프로토콜(미리 약속된 규칙)이다. 이 프로토콜을 준수한 객체는 for...of 문으로 순회할 수 있고 Spread 문법의

poiemaweb.com

이제 이 코드를 Generator로 변환해보자.

Generator로 변환

function* counter() {
  let count = 0;
  console.log(count);
  yield count++;                  
  console.log(count);
  yield count++;                 
  console.log('done'); 
}

const cnt = counter();

cnt.next(); // {value: 0, done: false}
cnt.next(); // {value: 1, done: false}
cnt.next(); // {value: undefined, done: true}

파이썬을 해봤다면 yield 문에 대해 한번은 들어봤을것이다.

우선 제네레이터에 대해 간략히 설명하자면

 

일반 함수와 같이 함수의 코드블록을 한번에 실행하지 않고 필요할때 재시작할 수 있는 특수한 함수이다.

value 값으로 yield 값이 리턴되고, done 값으로 언제 끝날지 알려주게 된다.

 

generator를 사용하면 

다음과 같은 방식으로도 구현 가능하다.

 

function *ex(){

  let a = yield;
  let b = yield;
  yield a+b;
}

const test = ex();

test.next();
test.next(1); // 인자전달
console.log(test.next(2)); // {value: 3, done: false}

시작지점을 알려준 뒤 

인자로 generator 안에 값을 넘겨줄수도 있다.

 

그럼 fibo 수열을 다시 generator로 구현해보자.

 

 function *fibonacci(){

  let [first, second] = [0,1];
  
  while(true){
    [first, second] = [second, first+second];
    yield first;
  }
}

const test = fibonacci();

test.next(); // 1
test.next(); // 1
test.next(); // 2
test.next(); // 3
...

코드가 매우 간략해진것을 확인 가능하다.

generator를 활용해서 무한 수열(Lazy Evaluation), 비동기 처리를 쉽게 활용 가능하다.

다만 좀 더 편리한 async/await 가 생겨서 generator 만으로 비동기 처리를 하지는 않는 편이다.

 

또한 redux-saga 등 여러 라이브러리에서 사용한다.

 

reference 

 

이터레이션 프로토콜

 

Iteration & for...of | PoiemaWeb

ES6에서 도입된 이터레이션 프로토콜(iteration protocol)은 데이터 컬렉션을 순회하기 위한 프로토콜(미리 약속된 규칙)이다. 이 프로토콜을 준수한 객체는 for...of 문으로 순회할 수 있고 Spread 문법의

poiemaweb.com

 

https://poiemaweb.com/es6-generator

 

Generator | PoiemaWeb

ES6에서 도입된 제너레이터(Generator) 함수는 이터러블을 생성하는 함수이다. 제너레이터 함수를 사용하면 이터레이션 프로토콜을 준수해 이터러블을 생성하는 방식보다 간편하게 이터러블을 구

poiemaweb.com

 

반응형
반응형

 

*잘못된 부분이 있으면 피드백 주시면 빠르게 수정하겠습니다!

 

ES5까지는 변수를 선언하는 방법은 var 밖에 없었습니다.

다만 변수의 유효범위를 설정하는데 어려움을 겪어서 ES6 이후 지역변수를 다루기 위해서 let, const가 등장하게 됩니다.

(var는 추가적으로 블록 스코프가 아닌 '함수 스코프' 개념을 따릅니다.)

자세한건 아래 링크를 참고하세요.

 

https://poiemaweb.com/js-scope

 

Scope | PoiemaWeb

스코프는 참조 대상 식별자(identifier, 변수, 함수의 이름과 같이 어떤 대상을 다른 대상과 식별할 수 있는 유일한 이름)를 찾아내기 위한 규칙으로 자바스크립트는 이 규칙대로 식별자를 찾는다.

poiemaweb.com

 

 

호이스팅

 

JavaScript에서 코드를 실행하기 전 평가실행 과정을 거칩니다.​

그리고 변수는 위의 사진처럼 선언, 초기화, 할당이라는 3가지 단계를 거쳐 생성됩니다.

 

선언 단계(Declaration phase) : 변수를 실행 컨텍스트의 변수 객체에 등록하는 단계를 의미합니다. 이 변수 객체는 스코프가 참조하는 대상이 됩니다.

초기화 단계(Initialization phase) : 실행 컨텍스트에 존재 하는 변수 객체에 선언 단계의 변수를 위한 메모리를 만드는 단계 입니다. 이 단계에서 할당된 메모리에는 undefined로 초기화 됩니다.

할당 단계(Assignment phase) : 사용자가 undefined로 초기화된 메모리의 다른 값을 할당하는 단계 입니다.

모든 변수는 생성되면 위 3단계를 거쳐 생성됩니다.

그래서 자바스크립트가 실행되면 모든 변수들은 평가단계를 거치게 되는데, 호이스팅이란 모든 변수들을 끌어 모아서 각 유효범위(블록 {}안)에 최상단에 선언하는것을 말합니다.

var, let, const 모두 위 3단계를 거칩니다.

다만 var는 선언단계와 초기화 단계가 호이스팅 단계에서 동시에 실행됩니다.

그래서 아직 할당이 되지 않았는데 var로 생성된 변수를 부르게 되면 undefined를 되돌려 줍니다.

let,const는 선언은 되지만 초기화는 되지 않고, 할당이 되기 전까지 TDN(temporal dead zone)이라는 곳에 등록됩니다. 그래서 할당이 되지 않았는데 불러온다면 TDN 구간에 있는 변수들은 참조 에러(Reference Error)를 발생시키게 됩니다.

즉, 호이스팅은 모두 발생하지만 var와 let,const는 다루는 방법에 차이가 있게 되는것입니다.

다른 언어에서 흔히 쓰는 변수들은 구조적으로는 약간씩 다르겠지만 결론적으로 let, const와 동일하게 쓰인다고 이해하시면 편합니다.

참고로 함수 선언문은 위 3가지가 동시에 진행됩니다.

함수 선언식 예시

 

function func { ... }

 

그래서 어디서 선언하든 유효 범위 (스코프) 내에서는 사용할 수 있습니다.

함수 표현식 예시

함수 표현식은 변수처럼 동작하고 호이스팅 됩니다.

const func = () => { ... }
 

따라서 var, const, let중 뭘 썼는지에 따라 호이스팅되고 변수처럼 처리됩니다.

호이스팅을 하는 이유는 현재의 실행 컨텍스트의 지금 스코프 내부에 어떤 식별자들이 있는지 탐색하고, 렉시컬 환경에 저장(기록) 해놓기 위해서 입니다.

 

즉.. 자바스크립트의 모든 핵심 개념이 서로 연동되있다고 볼 수 있을꺼 같은데요

자세히 알아보시려면 JavaScript deep dive란 책을 읽어보시길 추천 드립니다!

 

 

그리고 이거에 대해서 재밌는 문제가 하나 있는데요,

클로저 개념과 연동되는 문제이니 한번 생각해 보시면 좋습니다.

 

for(var i =0; i <100; i+=1){
    setTimeout(function(){
        console.log(i);
    },i*1000);
}

해당 코드를 실행시키면 0, 1, 2, 3, 4가 출력되는게 아니라 100, 100, 100... 이 출력되게 되는데

원래 의도대로 고치려면 어떻게 해야할까요?

 

 

더보기
for(let i =0; i <100; i+=1){
    setTimeout(function(){
        console.log(i);
    },i*1000);
}

 

해설은 다음 글을 참고하세요.

 

https://www.bangseongbeom.com/javascript-var-let.html

 

자바스크립트 var, let 차이점

자바스크립트에서 변수를 선언할 때 사용하는 var, let, const의 차이와 스코프, 호이스팅에 대해 알아봅시다.

www.bangseongbeom.com

 

 

 

반응형
반응형

*잘못된 부분이 있으면 피드백 주시면 감사하겠습니다!

 

자바스크립트에서 타이머 이벤트를 쓸 수 있는 방법으로는 setTimeout, setInterval 두가지가 있다.

 

이 함수들이 어떻게 비동기를 지원할까?

 

자바스크립트는 단일 쓰레드 기반의 언어이지만 이벤트 루프를 사용해서 동시성을 지원할 수 있다.

타이머를 실행시키면 Web API에서 타이머의 기한이 끝날때까지 대기하다가 테스크 큐로 들어오게 된다.

콜스택이 비었다면 테스크 큐에서 테스크를 가져와 실행한다.

 

이 말이 무엇인가를 다시 풀어써보기로 하자.

우선 Web API가 뭔지부터 알아야 할것 같다.

Web API 와 Javascript 런타임 

런타임이란 해당 프로그램 언어가 작성된 코드가 구동되는 환경을 뜻하고, 웹 브라우저나 Node.js 가 대표적 JS 런타임이다. 그리고 JS 이외에 다양한 기능을 제공하기 위해서 브라우저에서는 웹 API가 여러 함수(setTimeout등)을 지원하고, Node에서는 화면의 예시처럼 fs 등 함수를 지원하게 된다. 

 

브라우저 환경 구조 

자바스크립트 엔진

 

- Heap : 객체들은 힙 메모리에 할당된다. 크기가 동적으로 변하는 값들의 참조 값을 갖고 있다.
- Call Stack : 함수 호출 시, 실행 컨텍스트가 생성되며, 이러한 실행 컨텍스트들이 콜 스택을 구성한다.

 

Web API or Browser API


- 웹 브라우저에 구현된 API이다.
- DOM event, AJAX, Timer 등이 있다.

 

이벤트 루프

- 콜 스택이 비었다면, 태스크 큐에 있는 콜백 함수를 처리한다. (이후 자세히 설명)

 

태스크 큐

- 비동기 함수(setTimeout, setInterval 등)는 처리 전 이곳에서 대기하게 된다. 
- 이벤트 루프는 하나 이상의 태스크 큐를 갖는다.
- 태스크 큐는 태스크의 Set이다.
- 이벤트 루프가 큐의 첫 번째 태스크를 가져오는 것이 아니라, 태스크 큐에서 실행 가능한(runnable) 첫 번째 태스크를 가져오는 것이다. 태스크 중에서 가장 오래된 태스크를 가져온다.

 

(태스크 큐 관련해서 Microtask Queue, Animation Frames라는 세부적인 개념도 있는데 생략한다)

 

참고)

https://velog.io/@titu/JavaScript-Task-Queue%EB%A7%90%EA%B3%A0-%EB%8B%A4%EB%A5%B8-%ED%81%90%EA%B0%80-%EB%8D%94-%EC%9E%88%EB%8B%A4%EA%B3%A0-MicroTask-Queue-Animation-Frames-Render-Queue

 

[JavaScript] Task Queue말고 다른 큐가 더 있다고? (MicroTask Queue, Animation Frames)

자바스크립트에서 비동기 함수가 동작하는 원리에 대해서 공부했다면, Task Queue에 대해 들어보았을 것이다. Task Queue는 Web API가 수행한 비동기 함수를 넘겨받아 Event Loop가 해당 함수를 Call Stack에

velog.io

 

싱글 쓰레드와 이벤트 루프

 

setTimeout같은 타이머는 JS 엔진이 아니라 외부에 구현되어 있다!

 

그리고, 자바스크립트가 단일 쓰레드 기반이라는 말이 정확히는 '자바스크립트 엔진'은 단일 호출 스택을 사용한다 라는 것이다.

 

위 그림같은 경우는 Node.js는 비동기 IO를 지원하기 위해 libuv 라이브러리를 사용하며, 이 libuv가 이벤트 루프를 제공한다. 브라우저에서는 Web API가 setTimeout 같은 함수를 지원하며, 타이머를 JS의 싱글 쓰레드가 아닌, 브라우저의 자체 쓰레드에서 세다가 자바스크립트의 테스크 큐에 넣어주게 된다. 

 

즉, 실제 자바스크립트가 구동되는 환경(브라우저, Node.js)는 여러개의 쓰레드를 사용하고, 이 다중 쓰레드 환경과 자바스크립트 엔진이 상호 연동하기 위해 내부적으로 이벤트 루프를 이용해 처리하게 된다.

 

(또 다른 예시로는 Node.js에서 I/O를 처리할때 자바스크립트의 싱글 쓰레드로만 동작하는게 아니고, 커널 쓰레드에서 처리하는것?이라 볼 수 있을것 같다?)

 

그럼 자바스크립트가 어떻게 비동기 함수를 처리하는지, 이벤트 루프를 어떻게 사용하는지 예시를 보자

 

function delay() {
    for (var i = 0; i < 100000; i++);
}
function foo() {
    delay();
    bar();
    console.log('foo!'); // (3)
}
function bar() {
    delay();
    console.log('bar!'); // (2)
}
function baz() {
    console.log('baz!'); // (4)
}

setTimeout(baz, 10); // (1)
foo();

 

위 코드를 작동시키면 setTimeout으로 타이머를 걸어놓은 baz가 정확히 10ms 이후에 실행되지 않는다.

 

그 이유는 자바스크립트가 비동기 함수를 지원하는 방식은 '이벤트 루프'를 사용해서 구현하고,

이벤트 루프는 '콜스텍에 현재 실행중인 task가 없는지'와 'task 큐에 태스크가 있는지'를 반복적으로 확인한다.

그리고 현재 실행중인 테스크가 없다면 이제서야 테스크 큐에 담겨있는 테스크를 꺼내와서 실행하게 되는것이다.

즉, 콜스택이 지금 바쁘다면 타이머 함수는 제 시간에 실행되지 않을 수 있다.

 

이를 하나의 함수가 실행이 완료되기 전에는 다른 함수가 실행될 수 없는 "Run to Completion" 라 부른다.

 

아래 코드 또한 좋은 예시이다.

let i = 0;

setTimeout(() => alert(i), 100); // ?

// 아래 반복문을 다 도는 데 100ms 이상의 시간이 걸린다고 가정합시다.
for(let j = 0; j < 100000000; j++) {
  i++;
}

실제 실행해보면 100000000이 나오게 된다.

 

(그나마?) 정확한 타이머를 구현하는 방법은?

 

(이 방법말고 다른 좋은 방법도 추천 받습니다!)

스택오버플로우 형님들의 경우는 아래의 코드와 비슷한 방식을 이용한다.

 

https://stackoverflow.com/questions/29971898/how-to-create-an-accurate-timer-in-javascript 

const interval = INTERVAL; // 1000ms
let time = Date.now() + interval;
let timerId = -1;

const exactlyTimeInterval = () => {
      const dt = Date.now() - time;
      time += interval;
		
      if (dt > interval) {
        // something really bad happened. Maybe the browser (tab) was inactive?
        // possibly special handling to avoid futile "catch up" run
        // 브라우저 말고 다른곳을 보고 있다면 브라우저의 타이머가 멈춰있고, 제대로 작동하지 않는다.
        // 그때 이 if문 안에 들어오게 된다
      }
      ...(생략)
      timerId = setTimeout(exactlyTimeInterval, Math.max(0, interval - dt));
    };
    
timerId = setTimeout(exactlyTimeInterval, interval);

 

setTimeout를 재귀적으로 실행하면 다음과 같이 function이 끝난 후 다음 계획을 실행한다.

setTimeout 예시

그래서 Date 객체의 now 값을 이용해서 실시간으로 다음 계획까지 걸리는 타이머를 보정받는다.

콜스택에 의해 지금 function의 실행이 늦었다면 다음 실행의 경우 늦은만큼 보정한다.

 

하지만 브라우저가 inactive(다른 화면을 보고있다던지)하게 되면 타이머는 정지되게 되고, 다시 브라우저가 활성화된다면 그때 황급히 카운트를 쭉 세게 된다.

 

100% 완벽한가? 까지는 아니지만 괜찮은 방법인거 같다.

 

함께 읽어보면 좋은 글)

 

운영체제의 프로세스 - 쓰레드 구조가 실제로 크롬 브라우저에서 어떻게 쓰이는지 

https://developers.google.com/web/updates/2018/09/inside-browser-part1?hl=ko 

 

모던 웹 브라우저 들여다보기 (파트 1)  |  Web  |  Google Developers

브라우저에서 사용자 코드를 하이레벨 아키텍처부터 렌더링 파이프라인 세부 기능에 이르는 기능성 웹사이트로 전환하는 방법

developers.google.com

 

reference 

 

https://meetup.toast.com/posts/89

 

자바스크립트와 이벤트 루프 : NHN Cloud Meetup

자바스크립트와 이벤트 루프

meetup.toast.com

 

https://velog.io/@yejineee/%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%A3%A8%ED%94%84%EC%99%80-%ED%83%9C%EC%8A%A4%ED%81%AC-%ED%81%90-%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C-%ED%83%9C%EC%8A%A4%ED%81%AC-%EB%A7%A4%ED%81%AC%EB%A1%9C-%ED%83%9C%EC%8A%A4%ED%81%AC-g6f0joxx

 

이벤트 루프와 태스크 큐 (마이크로 태스크, 매크로 태스크)

자바스크립트는 싱글 스레드 기반의 언어이고, 자바스크립트 엔진은 하나의 호출 스택만을 사용한다. 이는 요청이 동기적으로 처리되어, 한 번에 한 가지 일만 처리할 수 있음을 의미한다. 만약

velog.io

 

https://ko.javascript.info/settimeout-setinterval

 

setTimeout과 setInterval을 이용한 호출 스케줄링

 

ko.javascript.info

https://stackoverflow.com/questions/29971898/how-to-create-an-accurate-timer-in-javascript

 

How to create an accurate timer in javascript?

I need to create a simple but accurate timer. This is my code: var seconds = 0; setInterval(function() { timer.innerHTML = seconds++; }, 1000); After exactly 3600 seconds, it prints about 3500 s...

stackoverflow.com

 

반응형

'Front-end > JavaScript' 카테고리의 다른 글

pnpm 도입기 (feat: 모노 레포)  (0) 2023.07.15
자바스크립트의 async/await에 대해서  (0) 2022.03.31
JS로 Single Page Application 구현  (0) 2022.03.12
Generator  (0) 2022.02.02
호이스팅과 var, let에 대하여  (0) 2022.01.29

+ Recent posts