본문 바로가기

웹 개발/프론트엔드 - JavaScript

[JavaScript] 한 달 동안 자바스크립트 박살내기 (10) - 모듈/오류처리, JS핵심 체크포인트

「한 달 동안 자바스크립트 박살내기」는, ChatGPT쨩이 선생님이 되어, 매일 수업해주는 느낌으로 진행합니다.

GPT쨩의 말은 이렇게 작성합니다.

 

 

 

오늘 공부 내용 및 목표

 

오늘 학습 목표

오늘은 지금까지 배운 JavaScript 핵심을 한 번 묶으면서, 코드를 파일 단위로 나누는 방법과 오류를 다루는 기본기를 배울 거야. 새 진도를 많이 벌리기보다는 ESM, IIFE/module pattern, try/catch/finally, Error를 정확히 이해하고, 지난 1주일간의 실행 모델을 연결하는 날이라고 보면 돼.

특히 오래된 대규모 웹 코드에서는 “이 값이 어디서 왔는지”, “어느 파일이 어느 파일에 의존하는지”, “오류가 어디서 만들어져 어디서 처리되는지”를 읽는 능력이 중요해. 오늘은 그 흐름을 읽는 기초를 잡자.

오늘은 약간 복습 위주가 될 수 있을 것 같다.

 

오늘의 위치

단계: JavaScript 핵심 1단계 마무리
주제: 모듈 / 오류 처리 + JS 핵심 체크포인트
필수 예상시간: 약 110~120분
선택 심화: 15~20분

 

 

 

어제 내용 복습

 

복습 1

다음 코드에서 counterA와 counterB가 서로 다른 count를 가지는 이유를 설명해봐.
function createCounter() {  
 let count = 0;  
  
 return () => ++count;  
}  
  
const counterA = createCounter();  
const counterB = createCounter();

각각 createCounter()로 만들어져, 다른 lexical environment를 가지기 때문이다.

정답.

 

 

복습 2

다음 코드의 출력은 무엇일까?
const character = {  
 name: "Lyria",  
  
 normal() {  
   console.log(this.name);  
 },  
};  
  
character.normal();

Lyria가 출력된다.

정답.

 

 

복습 3

다음 코드에서 child.run()이 실행되는 이유를 설명해봐.
const parent = {  
 run() {  
   console.log("run");  
 },  
};  
  
const child = Object.create(parent);  
child.run();

child는 parent를 프로토타입으로 만들어졌는데, child.run()을 실행하면, child에 run()이 없기 때문에 프로토타입인 parent에서 run()을 찾게 되고, parent에는 run()이 존재하기 때문에 그것을 실행한다.

정답.

 

 

복습 4

다음 코드에서 action에는 무엇이 저장될까?
function getAction() {  
 return function () {  
   console.log("attack");  
 };  
}  
  
const action = getAction();

attack을 출력하는 함수 자체가 저장된다.

정답.

 

 

 

핵심 수업

 

module - 관련된 코드와 값을 하나의 단위로 묶고, 필요한 것만 외부에 공개하는 구조

module(모듈)은 관련된 함수, 변수, 객체 등을 하나의 단위로 묶어 관리하는 방식이야.
코드가 작을 때는 한 파일에 전부 적어도 괜찮지만, 파일이 커지면 이름 충돌과 의존 관계 추적이 어려워져.
예를 들어 전투 계산과 화면 코드를 같은 파일에 다 넣으면 이런 식으로 뒤섞이기 쉬워.
const player = { hp: 100 };  
  
function calculateDamage(base) {  
 return base * 2;  
}  
  
function renderHp() {  
 console.log(player.hp);  
}
module을 쓰면 역할별로 나눌 수 있어.

battle.js   → 데미지 계산
ui.js       → 화면 관련 코드
main.js     → 둘을 연결

핵심은 단순히 “파일을 나눈다”가 아니라 어떤 코드를 외부에 공개하고 어떤 코드는 내부에 숨길지 경계를 만든다는 데 있어.

파일 분할에 대한 이야기인데, 어떤 코드를 공개하고 숨길지도 생각해야 한다고 한다.

 

 

ESM - JavaScript 표준 모듈 시스템

ESM(ECMAScript Modules)은 JavaScript가 공식적으로 제공하는 표준 module 방식이야.
ESM에서는 어떤 파일이 값을 외부에 공개할 때 export, 다른 파일이 그 값을 사용할 때 import를 써.

파일 외부에서 참조할 수 있게 할 때엔 선언시에 export를 붙여주고, 그걸 가져다 쓸 때는 import로 가져오는 걸 말한다.

 

// battle.js  
export function calculateDamage(base) {  
 return base * 2;  
}  

// main.js  
import { calculateDamage } from "./battle.js";  
  
console.log(calculateDamage(50)); // 100

 

 

IIFE - 정의하자마자 즉시 실행하는 함수 표현식

IIFE(Immediately Invoked Function Expression, 즉시 실행 함수 표현식)는 함수를 만든 직후 바로 호출하는 형태야.
가장 기본 형태는 이거야.
(function () {  
 const message = "battle start";  
 console.log(message);  
})();

이렇게 작성해주면, 함수가 정의된 뒤 바로 호출까지 된다.

 

ESM이 널리 쓰이기 전에는 파일 전체의 변수들이 전역 영역에 노출되기 쉬웠어. IIFE로 코드를 함수 scope 안에 넣으면 내부 변수가 바깥으로 새어나가지 않게 할 수 있었어.
(function () {  
 const hp = 100;  
 console.log(hp); // 100
})();  
  
console.log(hp); // ReferenceError

이렇게 하면, hp는 IIFE 내부 스코프에만 존재한다.

 

 

module pattern - closure를 이용해 내부 상태를 숨기고 필요한 기능만 공개하는 패턴

module pattern(모듈 패턴)은 함수 scope와 closure를 이용해서 내부 변수는 숨기고, 외부에서 사용할 함수만 객체로 반환하는 방식이야.
const battleState = (function () {  
 let hp = 100;  
  
 return {  
   damage(amount) {  
     hp -= amount;  
   },  
  
   getHp() {  
     return hp;  
   },  
 };  
})();  
  
battleState.damage(30);  
console.log(battleState.getHp()); // 70  
console.log(battleState.hp); // undefined

closure 내용과 겹치는 부분이 많은데, 코드를 보면,

battleState에는 IIFE가 실행되어 반환된 객체가 저장되는데, 그 객체에는 hp는 들어가있지 않고, damage()랑 getHp(), 두 메소드가 들어가있다.

그래서 hp에 직접 접근하는 것은 막고, damage()와 getHp()를 통해서만 접근할 수 있도록 캡슐화를 한 형태라고 볼 수 있다.

 

 

Error와 throw

Error(에러 객체)는 “무엇이 잘못됐는지”에 대한 정보를 담는 객체야.
가장 작은 예제부터 보면:
const error = new Error("HP는 0보다 작을 수 없습니다.");  
console.log(error.message);
error.message에는 우리가 넣은 설명 문자열이 들어 있어.
Error 객체를 쓰는 이유는 그냥 문자열 하나보다 오류라는 의미를 명확하게 표현하고, 오류 처리 흐름과 함께 사용할 수 있기 때문이야.

덧붙여, Error 객체를 상속해서 어떤 상황에 쓰는 전용 에러를 만드는 것도 가능하다.

 

throw(던지기)는 현재 코드 흐름에서 오류가 발생했다고 알리고 정상 실행을 중단하는 문법이야.
function setHp(hp) {  
 if (hp < 0) {  
   throw new Error("HP는 0보다 작을 수 없습니다.");  
 }  
  
 return hp;  
}

reference error 등으로, 시스템이 발생시키는 에러도 있지만, throw를 사용하면 프로그래머가 코드 상에 의도적으로 error를 발생시키는 것도 가능하다.

코드 상으로, hp가 0보다 낮으면, throw를 만나면서 그대로 흐름이 중단된다.

이렇게 발생된 error는 뒤에서 언급할 try~catch 문법으로 처리하게 된다.

 

 

try~catch - 오류가 발생할 수 있는 코드를 실행하고, 발생한 오류를 처리하는 구조

try는 “이 코드를 실행해봐” 영역이고, catch는 그 실행 중 오류가 발생했을 때 처리하는 영역이야.
function setHp(hp) {  
 if (hp < 0) {  
   throw new Error("HP는 0보다 작을 수 없습니다.");  
 }  
  
 return hp;  
}  
  
try {  
 const hp = setHp(-10);  
 console.log(hp); // 실행되지 않음
} catch (error) {  
 console.log("오류:", error.message);  
}

에러가 발생할 위험성이 있는 부분에 try를 씌워서, 국소적으로 에러를 처리하는 것이 가능하다.

이 예시를 보면, setHp 함수 내에서 hp가 0보다 낮으면 에러를 발생시키도록 되어있다.

try 내에서 setHp의 hp로 -10을 전달하고 있어 에러가 발생하고, 에러가 발생하자마자 try 안에 있는 나머지 코드는 실행되지 않고, 바로 catch 안으로 들어가게 된다.

 

finally(마지막 처리 영역)는 try가 성공하든 catch로 넘어가든 마지막에 실행되는 블록이야.
try {  
 console.log("battle start");  
} catch (error) {  
 console.log("error");  
} finally {  
 console.log("cleanup");  
}

이렇게 하면, try 안에서 에러가 나면 catch 블록으로 넘어간뒤 finally 블록이 실행된다.

에러가 안나면, catch 블록으로 넘어가지 않고 바로 finally 블록이 실행된다.

 

 

 

코드 보고 동작 예측하기

 

예측 1

function getHp(hp) {  
 if (hp < 0) {  
   throw new Error("invalid hp");  
 }  
  
 return hp;  
}  
  
try {  
 console.log(getHp(50));  
} catch (error) {  
 console.log("caught");  
}

50이 출력된다. getHp의 인수로 0 이상의 값이 전해져, 에러를 던지지 않기 때문.

정답.

 

 

예측 2

function getHp(hp) {  
 if (hp < 0) {  
   throw new Error("invalid hp");  
 }  
  
 return hp;  
}  
  
try {  
 console.log("A");  
 console.log(getHp(-1));  
 console.log("B");  
} catch (error) {  
 console.log(error.message);  
}

A, invalid hp가 출력된다.

A가 출력된 뒤에 getHp(-1)이 호출되면 error가 발생하고, 그 즉시 catch 블록으로 넘어가기 때문.

정답.

 

 

예측 3

try {  
 console.log("start");  
 throw new Error("failed");  
} catch (error) {  
 console.log("catch");  
} finally {  
 console.log("finally");  
}

start, catch, finally 가 출력된다.

정답.

 

 

예측 4

const state = (function () {  
 let hp = 100;  
  
 return {  
   damage(amount) {  
     if (amount < 0) {  
       throw new Error("invalid damage");  
     }  
  
     hp -= amount;  
   },  
  
   getHp() {  
     return hp;  
   },  
 };  
})();  
  
try {  
 state.damage(30);  
 state.damage(-10);  
} catch (error) {  
 console.log(error.message);  
}  
  
console.log(state.getHp());

우선, state에는 IIFE로 바로 함수가 호출되어, damage()와 getHp()가 퐇마된 객체가 저장된다.

try 블록 안해서 damage(30)이 실행되어, hp는 70이 된다.

뒤이어 damage(-10)이 실행되면, damage() 내에서 에러가 발생해 catch블록으로 이동한다.

그러면 error.message인 invalid damage가 출력되고, 마지막 console.log에선 hp인 70이 출력된다.

정답.

 

 

 

직접 코딩

 

작은 전투 상태 module 만들기

오늘은 작은 전투 상태 module을 만들어보자.
파일 2개를 만든다고 생각해.
// battleState.js
let hp = 100;  
  
// TODO 1: 현재 hp를 반환하는 getHp를 만들고 export  
  
// TODO 2: damage(amount)를 만들고 export  
// amount가 0보다 작으면 Error를 throw  
// 정상 값이면 hp에서 amount만큼 빼기
// main.js
// TODO 3: battleState.js에서 getHp, damage를 import  
  
try {  
 // TODO 4: damage를 정상 값으로 한 번 호출  
 // TODO 5: damage를 잘못된 값으로 한 번 호출  
} catch (error) {  
 // TODO 6: error.message 출력  
} finally {  
 // TODO 7: 현재 hp 출력  
}

아래와 같이 작성해주었다.

 

// battleState.js
let hp = 100;

export const getHp = () => hp;

export const damage = (amount) => {
  if (amount < 0) {
    throw new Error("데미지 값이 이상");
  }

  hp -= amount;
}
// main.js
import { damage, getHp } from "./battleState.js";

try {
  damage(10);
  damage(-10);
} catch (error) {
  console.log(error.message);
} finally {
  console.log(getHp());
}

index.html에서는, script 태그 안에 type=module을 추가해주어야 한다.

그래야 실행환경이 main.js를 모듈로 인식해준다.

 

 

코드 읽기 / 디버깅 드릴

const battle = (function () {  
 let hp = 100;  
  
 function damage(amount) {  
   if (amount < 0) {  
     throw new Error("damage must be positive");  
   }  
  
   hp -= amount;  
 }  
  
 return {  
   damage,  
   getHp() {  
     return hp;  
   },  
 };  
})();  
  
try {  
 battle.damage(20);  
 battle.damage(-5);  
 battle.damage(10);  
} catch (error) {  
 console.log(error.message);  
}  
  
console.log(battle.getHp());

 

1. 마지막 battle.damage(10)은 실행될까?
2. 최종 hp는 얼마일까?
3. hp를 battle.hp로 직접 읽을 수 없는 이유는 무엇일까?
4. 이 코드에서 closure가 사용되는 지점은 어디일까?
  1. 실행되지 않는다. battle.damage(-5)가 실행되려나 에러가 발생해 catch블록으로 이동하기 때문이다.
  2. 80.
  3. battle은 IIFE로 함수가 실행되면서 그 반환값이 저장되는데, 반환 객체에 hp는 포함되어있지 않기 때문이다.
  4. 객체를 반환하는 부분이다. damage와 getHp가 hp를 기억하고 있게 된다.

정답.

 

 

 

종료 테스트

 

1번 문제

try {  
 console.log("1");  
 throw new Error("boom");  
 console.log("2");  
} catch (error) {  
 console.log(error.message);  
} finally {  
 console.log("3");  
}

1, boom, 3.

2가 출력되기 전에 error가 발생해 catch블록으로 이동한다.

정답.

 

 

2번 문제

function createBattle() {  
 let hp = 100;  
  
 return {  
   damage(amount) {  
     if (amount > hp) {  
       throw new Error("too much damage");  
     }  
  
     hp -= amount;  
   },  
  
   getHp() {  
     return hp;  
   },  
 };  
}  
  
const battle = createBattle();  
  
try {  
 battle.damage(30);  
 battle.damage(80);  
} catch (error) {  
 console.log(error.message);  
}  
  
console.log(battle.getHp());

too much damage, 70이 출력된다.

damage(30)까진 실행되나, damage(80)이 실행될때 에러가 발생해, catch문으로 이동하기 때문이다.

정답.

 

 

 

감상

 

오늘은 비교적 익숙한 개념이라 수월했다. 라기엔 오늘 내용이 아니라 원래 8/31 내용이긴 한데.. 빨리 따라잡아야겠다.