Notice
Recent Posts
Recent Comments
Link
«   2024/12   »
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
Archives
Today
Total
관리 메뉴

heyday2024 님의 블로그

[React 1주차 (3)] 모듈, async, await 본문

프론트엔드 부트캠프

[React 1주차 (3)] 모듈, async, await

heyday2024 2024. 10. 29. 11:35

ES6 Modules

모듈은 재사용 가능한 코드 조각을 캡슐화하고 다른 자바스크립트 파일에서 쉽게 재사용할 수 있게함.

 

캡슐화:

자바스크립트 모듈은 관련된 함수, 변수, 객체, 클래스 등을 하나의 파일로 그룹화하고, 이를 모듈이라고 함. 이렇게 그룹화를 하게 되면 기능적으로 연관된 코드들을 묶어 관리할 수 있으며, 모듈 외부에서는 내부 구현을 알 필요 없이 제공되는 API를 통해 모듈을 사용할 수 있음!

캡슐 안에 중요한 정보와 로직을 넣어 보관!


 

ES6에선 export 라는 키워드를 사용해서 모듈을 만들고, 다른 파일에서 사용할 수 있게함.

모듈은 재사용할 수 있는 함수, 객체 또는 원시값을 의미함.

 

<기억해두기!>

  • 자바스크립트 실행환경: 브라우저 환경 & Node 환경
  • Node 환경에서의 모듈 import 방식은 2가지임!!
    1. CommonJS
      • 사용 환경: 주로 Node.js 환경에서 사용
      • 모듈 불러오기: require() 함수로 모듈을 불러옴
      • 모듈 내보내기: module.exports나 exports로 객체, 함수, 변수 등을 내보냄
      • 동기 방식: require()는 동기적으로 실행되어 파일을 즉시 불러옴
      • // module.js
        const greet = () => "Hello!";
        module.exports = greet;
        
        // main.js
        const greet = require("./module.js");
        console.log(greet()); // "Hello!"
    2. ES6 
      • 사용 환경: 최신 JavaScript 환경 (브라우저, Node.js 등)
      • 모듈 불러오기: import 키워드로 모듈을 불러옴
      • 모듈 내보내기: export 키워드로 내보낼 요소를 지정 (default export와 named export 모두 가능)
      • 비동기 방식: import는 비동기로 모듈을 불러오며, 이를 통해 최적화된 모듈 로딩 가능
      • // module.js
        export const greet = () => "Hello!";
        
        // main.js
        import { greet } from "./module.js";
        console.log(greet()); // "Hello!"

+ CommonJS는 주로 서버 사이드에서 사용해요! Node.js의 기본 모듈 시스템이구요. 다음과 같은 특징을 가진답니다.

  1. 동기적 로딩: CommonJS 모듈은 파일이 로컬 디스크에 있기 때문에 동기적으로 로딩됩니다. 이는 서버 환경에서는 문제가 되지 않지만, 브라우저 환경에서는 네트워크 지연으로 인해 문제가 될 수 있습니다.
  2. 단일 객체 내보내기: 모듈은 module.exports 객체를 통해 전체 모듈을 하나의 객체로 내보내며, 이 객체를 요구하는 다른 모듈은 require() 함수를 사용하여 가져옵니다.

왜 모듈을 사용해야할까???

1. 명확한 종속성 관리

전통적으로 script 태그를 사용하여 자바스크립트 파일을 로드하는 방식( 예를 들어, <script src="jquery.js"></script> )은 스크립트 파일 간의 종속성과 로딩 순서를 수동으로 관리해야 했음.

==>  이 방식은 프로젝트의 규모가 커질수록 종속성을 추적하고 관리하기 어려워지며, 실수로 인한 버그 발생 가능성을 높입니다.

 

 

// ES6 모듈 사용 예시
import $ from 'jquery';
import plugin from 'plugin'; // 자동으로 jQuery에 대한 의존성을 처리
import app from 'app'; // 모든 의존성이 충족되면 실행

그래서 모듈 시스템을 사용함으로서 각 모듈이 필요로 하는 종속성을 내부적으로 선언하기 시작함. 이로서 개발자는 파일을 로드하는 순서에 신경 쓸 필요가 없어짐.

 

  • 모듈 로더: JS 애플리케이션에서 모듈을 불러오고 관리하는도구 (ex) Webpack, Rollup, Parcel
    • 주요 역할:
      • 종속성 관리
        • 코드 내에서 import, export, require 등의 구문을 통해 연결된 모듈의 종속성을 분석
        • 올바른 로드 순서를 결정하여 의존성을 해결하고, 필요한 모듈만 불러옴.
      • 번들링
        • 종속성을 해석한 뒤 모듈을 하나의 파일로 묶어 배포에 최적화된 형태로 만들어냄.
        • 이를 통해 네트워크 요청 수를 줄이고, 애플리케이션 로딩 속도를 높임.
      • 최적화
        • 트리 쉐이킹(Tree Shaking): 사용하지 않는 코드를 제거해 번들 크기를 줄임
        • 코드 스플리팅: 필요 시 코드의 일부만 분리하여 로드하여 성능을 최적화

 

 

2. 코드 캡슐화와 충돌 방지

모듈은 자체적인 스코프를 가지므로, 모듈 외부에서는 모듈 내부의 변수에 직접 접근할 수 없음. 따라서 전역 변수의 오염을 방지할 수 있고, 이름이 서로 충돌하는 것을 막아줌.

 

예전 scirpt 태그 형식으로 js 파일 가져왔던 방식은 같은 전역변수 이름이나 함수 이름을 사용하면, 충돌이 일어났었다. 하나의 스코프로 생각하기 떄문에! 

하지만, 모듈 외부에서는 모듈 내부의 변수에 직접 접근할 수 없음으로 모듈화로 위의 문제를 해결할 수 있다!!!

예를 들어, 여러 스크립트에서 동일한 함수 이름을 사용하더라도 모듈을 사용하면 각 스크립트에서 정의된 함수는 해당 모듈 내에서만 유효함!

// module1.js
export function conflictFunction() {
  console.log('Module 1의 함수');
}

// module2.js
export function conflictFunction() {
  console.log('Module 2의 함수');
}

// app.js
import { conflictFunction as function1 } from './module1';
import { conflictFunction as function2 } from './module2';

function1(); // "Module 1의 함수"
function2(); // "Module 2의 함수"

3. 효율적인 코드 로딩

모듈 시스템을 통해 필요한 기능만 선택적으로 불러올 수 있다!!!

 

이러한 방식은 애플리케이션의 초기 로딩 시간을 단축시키고, 코드 스플리팅(code spliting) 을 사용하면 사용자의 현재 요구에 따라 필요한 코드만 동적으로 로드할 수 있음!

이러한 지연 로딩(lazy-loading)은 특히 대규모 애플리케이션에서 성능과 자원 사용을 최적화하는 데 매우 효과적임.

 

 

  • 코드 스플리팅: 코드 스플리팅은 애플리케이션 코드를 작은 단위로 나누어 필요한 시점에 로드하는 방식임으로, 초기 로딩 속도를 높이고 네트워크 요청을 최적화함.
import('./myComponent.js').then(module => {
    const myComponent = module.default;
    myComponent();
});

 

  • 지연 로딩: 지연 로딩은 코드 스플리팅으로 분리된 청크 중 필요한 자원만 요청하여 성능을 최적화함으로, 예를 들어 스크롤이나 클릭 같은 사용자 액션에 맞춰 리소스를 로드함.
button.addEventListener('click', () => {
    import('./heavyModule.js').then(module => {
        module.loadHeavyFeature();
    });
});

import 사용할 때 특정 요소를 다른 이름으로 지정해서 사용하고 싶을 때 as 사용!

(충돌을 방지하거나 식별자 명확성을 높일 수 있음)

 

export default : 모듈당 하나의 기본 내보내기를 정의할 떄 사용.

--> export default를 통해 내보내진 모듈은 import 시 이름변경이 가능함. 특정경로에서 가져온 값은 하나 밖에 존재하지 않기 떄문에 이름을 명시하지 않아도 특정할 수 있음.

 

// utils.js
export function add(a, b) {
    return a + b;
}

export function subtract(a, b) {
    return a - b;
}

const multiply = (a, b) => a * b;
export default multiply; // 기본 내보내기

 

// main.js
import multiply, { add, subtract } from './utils.js';

console.log(multiply(2, 3)); // 기본 내보내기
console.log(add(5, 2));      // 개별 내보내기
console.log(subtract(5, 2)); // 개별 내보내기

--> 이런 식으로 export default로 내보내진 요소는 중괄호 안에 넣지 않아도 됨.

 

분명 외부에서 가져오고 싶은 함수, 변수들이 매우 많을 수도 있다. 그 경우 중괄호 안에 하나하나 다 적어내기는 어려울 것이다.

 

이런 경우,

모듈 내용 전체 가져오고 싶을 때 * 를 사용!!

// app.js
import * as MathFunctions from './math.js';

console.log(MathFunctions.add(10, 5));       // 15
console.log(MathFunctions.multiply(10, 5));  // 50

 


Promise

Promise는 JavaScript에서 비동기 작업의 최종 완료 또는 실패를 나타내는 객체입니다. 이 객체는 비동기 작업이 수행될 때 그 결과값을 나중에 받기 위한 '약속'으로 사용됩니다. Promise는 주로 서버로부터 데이터를 요청하고 받아오는 HTTP 요청 처리에 사용되며, 파일 시스템 작업을 비롯한 다양한 비동기 작업에 활용됩니다.

Promise는 세 가지 상태 중 하나를 가집니다:

  • Pending (대기중): 초기 상태로, 아직 성공 또는 실패가 결정되지 않은 상태입니다.
  • Fulfilled (이행됨): 연산이 성공적으로 완료되어 프로미스가 결과 값을 반환한 상태입니다.
  • Rejected (거부됨): 연산이 실패하거나 오류가 발생한 상태입니다.

Promise 객체를 사용하면, 비동기 작업의 결과에 따라 콜백 함수를 연결할 수 있으며, .then(), .catch(), 그리고 .finally() 메소드를 이용해 연속적으로 결과를 처리할 수 있습니다. 이를 통해 콜백 지옥(Callback Hell)을 피하고 코드의 가독성을 높일 수 있습니다.

 

const myPromise = new Promise(function(resolve, reject) {
  // 비동기 작업을 수행하고
  if (/* 작업 성공 */) {
    resolve('Success!');
  } else {
    reject('Error!');
  }
});

myPromise.then(function(value) {
  // 성공(resolve)한 경우 수행
  console.log(value);  // 'Success!'
}).catch(function(error) {
  // 실패(reject)한 경우 수행
  console.error(error);  // 'Error!'
});

- promise.then다음 catch가 다 쓰일 수 있는 이유: then 도 promise 객체 반환하고, catch 도 promise 객체 반환해서 계속 chaining으로 쓸 수 있음.

- resolve, reject라는 이름보다 그 순서가 더 중요: 앞에가 성공했을 때, 뒤에가 실패했을 떄.

 

Await

await 키워드를 사용하면, 프로미스의 완료를 기다리는 동안 함수의 실행을 일시적으로 중단하고, 프로미스가 해결되면 자동으로 함수의 실행을 재개할 수 있습니다.

따라서, 비동기 코드의 동기적 표현이 가능합니다! 당연히 코드의 가독성이 크게 향상되겠죠.

async function fetchData() {
  try {
    const data = await fetch('https://api.example.com/data');
    const json = await data.json();
    console.log(json);
  } catch (error) {
    console.error("Data loading failed", error);
  }
}

fetchData();

--> async도 promise를 반환하기 때문에 함수 내부에서 await을 쓰지 않아도 기본적으로 프로미스를 반환함.

---> 그래서 then, catch 쓸 수 있음.

async function example() {
    return 42;
}

example().then(result => console.log(result)); // 42

 

그런데...

  • await을 사용하지 않는다면??
    • .then()과 .catch() 메서드를 사용하여 프로미스 체인을 구성해야함으로 반복적인 depth마다 복잡한 체인을 만들어 코드의 가독성을 저하시키고 유지보수가 어려워질 수 있음.
function fetchData() {
  return fetch('https://api.example.com/data')
    .then(response => response.json())
    .then(data => console.log(data))
    .catch(error => console.error("Data loading failed", error));
}

fetchData();