계산기 구현하기
Getting Started
Github 리포지토리에서 파일을 다운로드해 진행합니다. 이 레포지토리에는 어느 정도 완성된 계산기가 구현되어 있습니다. 튜토리얼을 참고하여 이 계산기의 가장 기본적인 연산 기능을 구현합니다.
Bare Minimum Requirements
Bare Minimum Requirements는 소프트웨어가 그 역할을 하기에 필요한 최소한의 요구사항입니다. 소프트웨어의 프로토타입을 빠르게 만들고, 프로토타입을 두고 기획자, 디자이너와 함께 회의를 하고 유저 경험을 개선할 수 있습니다. 개발자도 프로토타입을 생성하고 공유하고, 테스트하는 과정에서 이 소프트웨어에서 생길 수 있는 오류를 좀 더 빠르게 예측할 수 있습니다.
튜토리얼 1
버튼이 잘 눌렸는지 확인하기
먼저, calculator.html 파일을 열어 계산기를 확인합니다. 계산기에는 5가지 종류의 버튼이 있습니다.
- 숫자 버튼
- 연산자 (+, -, *, /) 버튼
- 소수점 버튼
- 계산 (Enter) 버튼
- 초기화 (AC) 버튼
계산기를 만들기 전에 이 버튼이 JavaScript에서 잘 작동하고 있는지 확인합니다. 크롬 개발자 도구의 Console 탭을 열고, 버튼을 클릭하면서 콘솔 메시지를 확인하세요. 잘 작동하나요? 5종류의 버튼이 모두 잘 작동해야 합니다. 만약, 소수점 버튼이 동작하지 않는다면, script.js 파일에서 직접 확인하세요.
script.js 파일을 수정하면, 실행시켜 둔 calculator.html 파일에서 버튼의 동작이 변경되는 모습을 확인할 수 있습니다. HTML 기본 구조와 문법에서 학습한 HTML의 <script> 요소가 JavaScript 파일을 연결하기 때문입니다. 앞으로는 script.js 파일만 열심히 수정하면 됩니다. 이후에 DOM에 대해 학습할 때, 자세히 다룹니다.
이제 calculator.html 파일의 버튼을 누르면, script.js 파일에서 작성한 함수가 동작한다는 것을 확인했습니다. 그리고 조건문에서 명시한 대로, 클릭하는 버튼의 종류에 따라 다른 함수를 작동시킬 수 있다는 것을 학습했습니다. 나중에 이벤트 객체와 이벤트 리스너에 대해 학습하면, 이 코드가 왜 작동하는지 조금 더 자세하게 이해할 수 있습니다.
buttons.addEventListener('click', function (event) {
// 버튼을 눌렀을 때 작동하는 함수입니다.
const target = event.target; // 클릭된 HTML 엘리먼트의 정보가 저장되어 있습니다.
const action = target.classList[0]; // 클릭된 HTML 엘리먼트에 클래스 정보를 가져옵니다.
const buttonContent = target.textContent; // 클릭된 HTML 엘리먼트의 텍스트 정보를 가져옵니다.
if (target.matches('button')) {
// 클릭된 HTML 엘리먼트가 button이면
if (action === 'number') {
// 그리고 버튼의 클래스가 number이면
// 아래 코드가 작동됩니다.
console.log('숫자 ' + buttonContent + ' 버튼');
previousKey = 'number';
}
// ...
}
// ...
}
[코드] 버튼이 클릭되었을 때 함수 내부의 코드를 실행하는 이벤트 리스너
파일을 수정하고 나면 Ctrl + S 혹은 Cmd + S를 눌러서 파일을 저장하세요. 습관을 들이는 것이 좋습니다. 저장은 변경 사항이 calculator.html 에 적용될 수 있도록 합니다. 동시에, 예기치 못한 상황으로부터 여러분의 멘탈을 지켜줍니다.
튜토리얼 2
기본 계산 기능 구현하기
- 순서에 따라 진행하세요.
화면의 첫 번째 칸에 숫자 나타내기
먼저, 숫자 버튼을 클릭했을 때, 화면에 숫자가 나와야 합니다. script.js 파일에서, 계산기의 화면에 보이는 작은 빈칸의 정보를 받아 변수에 저장했습니다. 특정 엘리먼트의 textContent를 불러와 출력하거나 새로운 값을 입력할 수 있습니다. 그 방법을 이용해 첫 번째 빈칸부터 숫자를 넣을 수 있게 구현합니다.
아직 잘 모르는 문법이 많을 수 있습니다. 그러나, 코드와 주석을 잘 읽어보면 대략적인 기능을 파악할 수 있습니다. 예를 들어 숫자 버튼을 클릭하면, 이 글의 아래에 있는 코드 블록에서 here 주석 아래에 있는 코드가 동작합니다. 첫 번째 조건문에서 버튼 엘리먼트를 찾고, 두 번째 조건문에서 버튼의 클래스가 number인지 검사합니다. 지금은 잘 모르셔도 괜찮습니다. console.log을 이용해 어떤 내용일 출력되는지 확인하면, 금방 알아차릴 수 있습니다. buttonContent라는 변수가 버튼 엘리먼트의 textContent를 담고 있어, 버튼에 적힌 숫자가 출력됩니다.
위 과정을 통해, 버튼을 클릭해 버튼에 적혀있는 숫자를 받아왔습니다. 받아온 숫자를 화면에 적용하기 위해서는, 변수 firstOperend에 담긴 HTML 엘리먼트의 textContent에 받아온 숫자를 입력하면 됩니다. 직접 해보세요!
/* ...초략... */
const firstOperend = document.querySelector('.calculator__operend--left');
/* ...중략... */
if (target.matches('button')) {
if (action === 'number') {
// here
console.log('숫자 ' + buttonContent + ' 버튼');
}
// ...
}
[코드] script.js 파일
화면의 두 번째 칸에 숫자 나타내기
첫 번째 칸에는 숫자를 성공적으로 입력했습니다. 이번에는 두 번째 숫자 칸에 다른 값을 넣어야 합니다. 다양한 방법으로 구현할 수 있지만, 이번 예제에서는 조건문으로 간단한 논리를 구현했습니다.
1. 첫 번째 칸에 입력된 내용이 있는지, 없는지 구분해야 합니다.
2. 첫 번째 칸에 입력된 내용이 0(기본값)이 아니라면, 이미 숫자가 입력된 상태로 볼 수 있습니다.
3. 첫 번째 숫자가 0이 아닌 경우, 버튼을 클릭하면 두 번째 칸에 버튼에 적혀있는 숫자를 입력합니다.
[텍스트] 의사코드(Pseudocode)
이렇게 실제로 코딩을 하기 전에 글로써 작성한 논리를 의사코드라고 합니다. 코드를 어떻게 작성할지 고민하기 전에, 문제를 해결하는 방법에 대해 먼저 고민하는 방법이자, 개발자가 코드를 작성하기 전 작업하는 일종의 밑그림(스케치)입니다.
script.js 파일에서 두 번째 칸에 숫자가 입력되도록 구현하세요. 아래와 같이 7, +, 5를 누른 결과가 잘 나오면 성공입니다 👏
- '연산 기호가 적혀있는 버튼'에 관한 코드는 지금 작성하지 않습니다.
- 제공된 스켈레톤 코드(뼈대가 되는 코드)에는, '연산 기호가 적혀있는 있는 버튼'에 관한 코드를 작성할 공간이 이미 마련되어 있습니다.
화면에 출력된 숫자와 연산자로 계산하기
숫자를 입력했으니, 이번에는 계산을 합니다. 제공된 script.js 파일에는 함수 calculate이 이미 선언되어 있습니다. 이 함수는 숫자 두 개(n1, n2)와 연산기호(operator)를 전달받습니다. 화면에 나타나는 두 숫자와 연산자로 사칙연산이 가능하도록 구현하세요. 마지막에 Enter 버튼을 클릭했을 때, = 뒤의 마지막 칸에 결과가 나타나야 합니다. script.js 파일 상단에는 구현에 필요한 변수(id 또는 클래스에 해당하는 HTML 요소)가 존재합니다.
1. 첫 번째 숫자, 연산자, 두 번째 숫자를 확정해야 합니다.
2. 위 세 가지를 함수 `calculate`에 전달하고, 돌려받은 결과값이 마지막 칸에 입력되어야 합니다.
위 의사코드를 직접 구현하세요.
- 함수를 호출하고, 결과값을 얻어내는 방법은 이미 연습했습니다. 만약 함수의 호출에 대한 이해도가 조금 부족한 것 같다면 함수 호출을 복습해 주세요.
아래의 그림처럼 7 + 5에 대한 계산 결과로 12가 나오면 성공입니다. 축하합니다!
화면상의 값을 초기화하기
계산기에는 AC(All Cancel) 버튼이 있습니다. 마찬가지로 AC 버튼을 클릭했을 때, 화면에 보이는 값을 초기화하세요. 이번에는 힌트가 제공되지 않습니다. 이번 챕터의 내용을 잘 따라오셨다면, 어렵지 않게 해내실 수 있습니다.
script.js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JavaScript Calculator</title>
<link href="https://fonts.googleapis.com/css?family=Righteous" rel="stylesheet">
<link rel="stylesheet" href="./yourStyle.css">
</head>
<body>
<div class="container">
<div class="calculator">
<div class="calculator__display--bare">
<span class="calculator__operend--left">0</span>
<span class="calculator__operator">+</span>
<span class="calculator__operend--right">0</span>
<span class="calculator__equal">=</span>
<span class="calculator__result">0</span>
</div>
<div class="calculator__display--for-advanced">0</div>
<div class="calculator__buttons">
<div class="clear__and__enter">
<button class="clear">AC</button>
<button class="calculate">Enter</button>
</div>
<div class="button__row">
<button class="number">7</button>
<button class="number">8</button>
<button class="number">9</button>
<button class="operator">+</button>
</div>
<div class="button__row">
<button class="number">4</button>
<button class="number">5</button>
<button class="number">6</button>
<button class="operator">-</button>
</div>
<div class="button__row">
<button class="number">1</button>
<button class="number">2</button>
<button class="number">3</button>
<button class="operator">*</button>
</div>
<div class="button__row">
<button class="number double">0</button>
<button class="decimal">.</button>
<button class="operator">/</button>
</div>
</div>
</div>
</div>
<script src='./script.js'></script>
</body>
</html>
SpecRunner.html
const calculator = document.querySelector('.calculator'); // calculator 엘리먼트와, 그 자식 엘리먼트의 정보를 모두 담고 있습니다.
const buttons = calculator.querySelector('.calculator__buttons'); // calculator__keys 엘리먼트와, 그 자식 엘리먼트의 정보를 모두 담고 있습니다.
const firstOperend = document.querySelector('.calculator__operend--left'); // calculator__operend--left 엘리먼트와, 그 자식 엘리먼트의 정보를 모두 담고 있습니다.
const operator = document.querySelector('.calculator__operator'); // calculator__operator 엘리먼트와, 그 자식 엘리먼트의 정보를 모두 담고 있습니다.
const secondOperend = document.querySelector('.calculator__operend--right'); // calculator__operend--right 엘리먼트와, 그 자식 엘리먼트의 정보를 모두 담고 있습니다.
const calculatedResult = document.querySelector('.calculator__result'); // calculator__result 엘리먼트와, 그 자식 엘리먼트의 정보를 모두 담고 있습니다.
function calculate(n1, operator, n2) {
let result = 0;
let first = Number(n1);
let second = Number(n2);
if (operator === "+") {
result = first + second;
} else if (operator === "-") {
result = first - second;
} else if (operator === "*") {
result = first * second;
} else if (operator === "/") {
result = first / second;
}
// TODO : n1과 n2를 operator에 따라 계산하는 함수를 만드세요.
// ex) 입력값이 n1 : '1', operator : '+', n2 : '2' 인 경우, 3이 리턴됩니다.
return String((calculatedResult.textContent = result));
}
buttons.addEventListener('click', function (event) {
// 버튼을 눌렀을 때 작동하는 함수입니다.
const target = event.target; // 클릭된 HTML 엘리먼트의 정보가 저장되어 있습니다.
const action = target.classList[0]; // 클릭된 HTML 엘리먼트에 클레스 정보를 가져옵니다.
const buttonContent = target.textContent; // 클릭된 HTML 엘리먼트의 텍스트 정보를 가져옵니다.
// ! 위 코드(Line 19 - 21)는 수정하지 마세요.
if (target.matches('button')) {
// TODO : 계산기가 작동할 수 있도록 아래 코드를 수정하세요. 작성되어 있는 조건문과 console.log를 활용하시면 쉽게 문제를 풀 수 있습니다.
// 클릭된 HTML 엘리먼트가 button이면
if (action === 'number') {
// 그리고 버튼의 클레스가 number이면
// 아래 코드가 작동됩니다.
console.log('숫자 ' + buttonContent + ' 버튼');
if (firstOperend.textContent === "0") {
firstOperend.textContent = buttonContent;
} else if (secondOperend.textContent === "0") {
secondOperend.textContent = buttonContent;
}
}
if (action === 'operator') {
console.log('연산자 ' + buttonContent + ' 버튼');
operator.textContent = buttonContent;
}
if (action === 'decimal') {
// console.log('소수점 버튼');
}
if (action === 'clear') {
console.log('초기화 버튼');
firstOperend.textContent = "0";
operator.textContent = "+";
secondOperend.textContent = "0";
calculatedResult.textContent = "0";
}
if (action === "calculate") {
console.log("계산 버튼");
let first = firstOperend.textContent;
let operators = operator.textContent;
let second = secondOperend.textContent;
calculate(first, operators, second);
}
}
});
// ! Advanced Challenge test와 Nightmare test를 위해서는 아래 주석을 해제하세요.
const display = document.querySelector('.calculator__display--for-advanced'); // calculator__display 엘리먼트와, 그 자식 엘리먼트의 정보를 모두 담고 있습니다.
let firstNum, operatorForAdvanced, previousKey, previousNum;
buttons.addEventListener('click', function (event) {
// 버튼을 눌렀을 때 작동하는 함수입니다.
const target = event.target; // 클릭된 HTML 엘리먼트의 정보가 저장되어 있습니다.
const action = target.classList[0]; // 클릭된 HTML 엘리먼트에 클레스 정보를 가져옵니다.
const buttonContent = target.textContent; // 클릭된 HTML 엘리먼트의 텍스트 정보를 가져옵니다.
// ! 위 코드는 수정하지 마세요.
// ! 여기서부터 Advanced Challenge & Nightmare 과제룰 풀어주세요.
if (target.matches('button')) {
if (action === 'number') {}
if (action === 'operator') {}
if (action === 'decimal') {}
if (action === 'clear') {}
if (action === 'calculate') {}
}
});
[JavaScript] 핵심 개념과 주요 문법
불과 얼마 전, 코딩의 첫걸음을 떼었을 때의 여러분과 지금의 여러분을 한 번 비교해 보세요. 여러분은 벌써 정말 많은 성장을 이루어 내셨습니다! 이번 유닛을 수학으로 비유하자면, 단순한 공식이 아닌 공식 내부의 원리를 파악하는 내용이라고 할 수 있습니다. 클로저, 스코프 등 JavaScript의 다소 깊은 원리를 이해해야 하는 내용들과 더불어, spread syntax, rest syntax 등의 새로운 문법도 다루게 됩니다.
이 유닛을 학습하고 나면 여러분은 예상치 못한 오류를 만났을 때 디버깅을 할 수 있는 능력을 갖출 수 있게 됩니다. 또 새로운 문법을 통해 더욱 효율적인 코드를 작성할 수 있게 됩니다. 난이도는 있는 편이지만 너무 미리 걱정하실 필요는 없습니다. 지금까지 해 온 것처럼 여러분에게 주어진 학습량을 충실하게 채워나간다면 충분히 이해할 수 있게 콘텐츠가 구성되어 있습니다.
Before You Learn
- 타입: 자료형에 대한 보다 심화적인 내용을 다루게 됩니다. Unit2에서 학습한 타입과 Unit9에서 학습한 배열과 객체에 대한 이해가 필요합니다.
- 함수: 이번 유닛에서 학습할 스코프와 클로저를 이해하기 위해서는 함수에 대한 기본적인 이해가 필수입니다.
Chapter1. 원시 자료형과 참조 자료형
참조 자료형인 배열과 객체를 학습하면서 원시 자료형과 달리 저장 공간이 계속 늘어날 수도 있을 것 같다는 생각이 드시지 않으셨나요? 만약 그런 질문이 생겼다면, 잘 학습하신 겁니다.
JavaScript 기초에서 학습한 number, string, boolean과 같은 자료형은 고정된 저장 공간을 차지합니다. 이런 특징을 가진 자료형을 원시 자료형(primitive data type)이라는 이름으로 분류합니다. 반면에 대량의 데이터를 다루기에 적합한 배열과 객체 등의 자료형은 참조 자료형(reference data type)이라고 분류합니다.
원시 자료형과 참조 자료형이 가진 각각의 특성에 대해 이 챕터를 통해 알아보겠습니다.
학습 목표
- 원시 자료형(primitive data type)과 참조 자료형(reference data type)의 구분이 왜 필요한지 이해할 수 있다.
- 원시 자료형과 참조 자료형의 차이를 이해하고, 각자 맞는 상황에서 사용할 수 있다.
- 원시 자료형이 할당될 때는 변수에 값(value) 자체가 담기고, 참조 자료형이 할당될 때는 보관함의 주소(reference)가 담긴다는 개념을 코드로 설명할 수 있다.
- 참조 자료형은 기존에 고정된 크기의 보관함이 아니라, 동적으로 크기가 변하는 특별한 보관함을 사용한다는 것을 이해할 수 있다.
- 참조 자료형인 값을 복사하는 방법에 대해서 이해한다.
Chapter1-1. 원시 자료형과 참조 자료형
개념학습
JavaScript에서 자료형(type)이란 값(value)의 종류입니다. 각각의 자료형은 고유한 속성과 메서드를 가지고 있습니다. 자료형은 크게 두 가지로 구분할 수 있는데, 바로 원시 자료형(primitive type)과 참조 자료형(reference type)입니다.
JavaScript에서는 6개의 자료형(number, string, boolean, undefined, null, symbol)을 원시 자료형으로 구분합니다. 이중 symbol 타입은 잘 사용되지 않는 타입이므로, 나머지 5개의 타입을 위주로 다루겠습니다.
// 원시 자료형(primitive type): number, string, boolean, undefined, null
42, 'string', true, undefined, null
원시 자료형이 아닌 모든 자료형은 참조 자료형입니다. 여러 데이터를 한 번에 다룰 수 있는 배열, 객체가 대표적인 참조 자료형입니다. 함수도 참조 자료형으로 분류합니다.
// 참조 자료형(reference type)
[0, 1, 2] // 배열
{name: 'kimcoding', age: 45} // 객체
function sum (x, y) { return x + y } // 함수
원시 자료형과 참조 자료형의 특징
원시 자료형과 참조 자료형의 특징은 크게 세 가지로 정리할 수 있습니다.
원시 자료형의 특징
- 원시 자료형을 변수에 할당하면 메모리 공간에 값 자체가 저장된다.
- 원시 값을 갖는 변수를 다른 변수에 할당하면 원시 값 자체가 복사되어 전달된다.
- 원시 자료형은 변경 불가능한 값(immutable value)이다. 즉, 한 번 생성된 원시 자료형은 읽기 전용(read only) 값이다.
참조 자료형의 특징
- 참조 자료형을 변수에 할당하면 메모리 공간에 주소값이 저장된다.
- 참조 값을 갖는 변수를 다른 변수에 할당하면 주소값이 복사되어 전달된다.
- 참조 자료형은 변경이 가능한 값(mutable value)이다.
[이미지] 원시 자료형과 참조 자료형의 특징
이처럼 원시 자료형과 참조 자료형의 특징은 서로 대비됩니다. 각각의 특징에 대해 보다 자세하게 알아보겠습니다.
1. 값 자체를 저장 vs 주소값을 저장
원시 자료형을 변수에 할당하면 값 자체가 할당됩니다. num이라는 변수를 선언하고 숫자 20을 할당하면 어떤 일이 일어날까요?
let num = 20;
변수 num을 선언하면 컴퓨터는 num이라는 이름의 공간을 확보합니다. 그리고 20이라는 원시 값을 그 공간에 저장합니다. 이를 그림으로 나타내면 아래와 같습니다.
num이라는 이름의 저장 공간에 원시 값 20이 저장되었습니다. 이처럼 원시 자료형은 값 자체를 저장합니다. 그렇다면 여러 개의 값을 다룰 수 있는 참조 자료형은 어떻게 값을 저장할까요? 예를 들어 arr이라는 변수에 0부터 3까지의 숫자를 요소로 가지고 있는 배열을 할당했다고 해보겠습니다.
let arr = [0, 1, 2, 3];
배열의 요소 각각이 하나의 값이기 때문에 하나의 공간에 배열 자체를 저장하는 것은 불가능합니다.
배열의 요소를 각각 하나의 공간에 저장한 후 같은 변수명을 부여하면 어떻게 될까요? 여러 개의 값이 저장되어 있는 공간에 같은 변수명이 부여되어 있으므로, 원하는 데이터를 조회하기 어려울 것입니다. 또 배열의 요소나 객체의 프로퍼티는 추가 및 삭제가 수시로 일어나고, 정해진 개수가 없기 때문에 이와 같은 형태도 바람직하다고 볼 수 없습니다.
그렇다면 JavaScript는 참조 자료형을 어떻게 저장할까요? JavaScript는 특별한 저장 공간에 참조 자료형을 저장한 후, 그 저장공간을 참조할 수 있는 주소값을 변수에 저장합니다. 이때 참조 자료형을 저장하는 특별한 저장 공간을 힙(heap)이라고 부르기도 합니다. 따라서 변수 arr에 해당하는 저장공간에는 주소값이 저장되어 있고, 그 주소값을 통해 참조 자료형에 접근할 수 있습니다. 이를 참조한다(refer)고 합니다.
2. 원시 값 자체를 복사 vs 주소값을 복사
만약 어떤 변수에 저장되어 있는 원시 자료형을 다른 변수에 할당하면 어떻게 될까요?
let num = 20;
let copiedNum = num;
원시 자료형은 값 자체가 복사됩니다. 즉, 변수 num과 변수 copiedNum은 동일하게 20이라는 값을 가집니다.
참조 자료형은 이와는 달리 주소값을 복사합니다.
let arr = [0, 1, 2, 3];
let copiedArr = arr;
즉 참조 자료형이 할당된 변수를 다른 변수에 할당하면, 이 두 변수는 같은 주소를 가리킵니다.
앞서 원시 자료형은 값 자체를 저장하고, 참조 자료형은 주소를 저장한다는 내용을 학습했기 때문에 다른 변수에 할당했을 때 원시 자료형은 값 자체가 복사가 되고, 참조 자료형은 주소가 복사되는 것은 특별할 것이 없어 보입니다. 그러나 원본을 변경하면 어떻게 될까요?
Chapter1-2. 얕은 복사와 깊은 복사
개념학습
원시 자료형을 할당한 변수를 다른 변수에 할당하면 값 자체의 복사가 일어납니다. 값 자체가 복사된다는 것은 둘 중 하나의 값을 변경해도 다른 하나에는 영향을 미치지 않는다는 것을 의미합니다.
let num = 5;
let copiedNum = num;
console.log(num); // 5
console.log(copiedNum); // 5
console.log(num === copiedNum); // true
copiedNum = 6;
console.log(num); // 5
console.log(copiedNum); // 6
console.log(num === copiedNum); // false
반면, 참조 자료형은 임의의 저장공간에 값을 저장하고 그 저장공간을 참조하는 주소를 메모리에 저장하기 때문에 다른 변수에 할당할 경우 값 자체가 아닌 메모리에 저장되어 있는 주소가 복사됩니다.
let arr = [0, 1, 2, 3];
let copiedArr = arr;
console.log(arr); // [0, 1, 2, 3]
console.log(copiedArr); // [0, 1, 2, 3]
console.log(arr === copiedArr) // true
따라서 둘 중 하나를 변경하면 해당 변수가 참조하고 있는 주소에 있는 값이 변경되기 때문에 다른 하나에도 영향을 미치게 됩니다. 예를 들어 배열을 할당한 변수 arr를 변수 copiedArr에 할당한 후, copiedArr에 push() 메서드를 사용하여 배열의 요소를 추가하면, 원본 배열인 arr에도 동일하게 요소가 추가됩니다. arr이 참조하고 있던 주소가 copiedArr로 복사되어, 두 변수가 같은 주소를 참조하고 있기 때문입니다.
copiedArr.push(4);
console.log(arr); // [0, 1, 2, 3, 4]
console.log(copiedArr); // [0, 1, 2, 3, 4]
console.log(arr === copiedArr) // true
다시 말해, 참조 자료형이 저장된 변수를 다른 변수에 할당할 경우, 두 변수는 같은 주소를 참조하고 있을 뿐 값 자체가 복사되었다고 볼 수 없습니다.
그렇다면 배열과 객체 같은 참조 자료형을 복사하여, 똑같은 요소와 프로퍼티를 가지지만 원본과 복사본이 서로 영향을 미치지 않도록 할 수는 없을까요?
summary
- 원시 자료형이 할당된 변수를 다른 변수에 할당하면 값 자체의 복사가 일어난다. 따라서 원본과 복사본 중 하나를 변경해도 다른 하나에 영향을 미치지 않는다.
- 참조 자료형이 할당된 변수를 다른 변수에 할당하면 주소가 복사되어 원본과 복사본이 같은 주소를 참조한다.
- 참조 자료형의 주소값을 복사한 변수에 요소를 추가하면 같은 주소를 참조하고 있는 원본에도 영향을 미친다.
- 참조 자료형이 저장된 변수를 다른 변수에 할당할 경우, 두 변수는 같은 주소를 참조하고 있을 뿐 값 자체가 복사되었다고 볼 수 없다.
배열 복사하기
배열을 복사하는 방법은 크게 두 가지 방법이 있습니다. 배열 내장 메서드인 slice()를 사용하는 방법과 ES6에서 도입된 spread문법을 사용하는 방법입니다.
slice()
배열 내장 메서드인 slice()를 사용하면 원본 배열을 복사할 수 있습니다.
let arr = [0, 1, 2, 3];
let copiedArr = arr.slice();
console.log(copiedArr); // [0, 1, 2, 3]
console.log(arr === copiedArr); // false
새롭게 생성된 배열은 원본 배열과 같은 요소를 갖지만 참조하고 있는 주소는 다릅니다.
주소가 다르기 때문에 복사한 배열에 요소를 추가해도 원본 배열에는 추가되지 않습니다.
copiedArr.push(4);
console.log(copiedArr); // [0, 1, 2, 3, 4]
console.log(arr); // [0, 1, 2, 3]
spread syntax
spread syntax는 ES6에서 새롭게 추가된 문법으로, spread라는 단어의 뜻처럼 배열을 펼칠 수 있습니다. 펼치는 방법은 배열이 할당된 변수명 앞에 ...을 붙여주면 됩니다. 배열을 펼치면 배열의 각 요소를 확인할 수 있습니다.
let arr = [0, 1, 2, 3];
console.log(...arr); // 0 1 2 3
spread syntax로 배열을 복사하기 위해서 배열을 생성하는 방법을 이해해야 합니다. 만약 같은 요소를 가진 배열을 두 개 만든 후 변수에 각각 할당한다면, 두 변수는 같은 주소를 참조할까요? 참조 자료형이기 때문에 각각 다른 주소를 참조합니다.
let num = [1, 2, 3];
let int = [1, 2, 3];
console.log(num === int) // false
그렇다면 새로운 배열 안에 원본 배열을 펼쳐서 전달하면 어떻게 될까요? 원본 배열과 같은 요소를 가지고 있지만 각각 다른 주소를 참조하게 됩니다. 결과적으로 slice() 메서드를 사용한 것과 동일하게 동작합니다.
let arr = [0, 1, 2, 3];
let copiedArr = [...arr];
console.log(copiedArr); // [0, 1, 2, 3]
console.log(arr === copiedArr); // false
copiedArr.push(4);
console.log(copiedArr); // [0, 1, 2, 3, 4]
console.log(arr); // [0, 1, 2, 3]
객체 복사하기
Object.assign()
객체를 복사하기 위해서는 Object.assign()을 사용합니다.
let obj = { firstName: "coding", lastName: "kim" };
let copiedObj = Object.assign({}, obj);
console.log(copiedObj) // { firstName: "coding", lastName: "kim" }
console.log(obj === copiedObj) // false
spread syntax
spread syntax는 배열뿐만 아니라 객체를 복사할 때도 사용할 수 있습니다.
let obj = { firstName: "coding", lastName: "kim" };
let copiedObj = {...obj};
console.log(copiedObj) // { firstName: "coding", lastName: "kim" }
console.log(obj === copiedObj) // false
그러나 예외의 상황도 있습니다. 참조 자료형 내부에 참조 자료형이 중첩되어 있는 경우, slice(), Object.assign(), spread syntax를 사용해도 참조 자료형 내부에 참조 자료형이 중첩된 구조는 복사할 수 없습니다. 참조 자료형이 몇 단계로 중첩되어 있던지, 위에서 설명한 방법으로는 한 단계까지만 복사할 수 있습니다.
유저의 정보를 담고 있는 객체를 요소로 가지고 있는 배열 users를 slice() 메서드를 사용하여 복사했습니다.
let users = [
{
name: "kimcoding",
age: 26,
job: "student"
},
{
name: "parkhacker",
age: 29,
job: "web designer"
},
];
let copiedUsers = users.slice();
users와 copiedUsers를 동치연산자(===)로 확인해 보면 false가 반환됩니다. 위에서 살펴본 바와 같이 각각 다른 주소를 참조하고 있기 때문입니다.
console.log(users === copiedUsers); // false
그러나 users와 copiedUsers의 0번째 요소를 각각 비교하면 true가 반환됩니다. users[0]과 copiedUsers[0]는 여전히 같은 주소값을 참조하고 있기 때문입니다.
console.log(users[0] === copiedUsers[0]); // true
이처럼 slice(), Object.assign(), spread syntax 등의 방법으로 참조 자료형을 복사하면, 중첩된 구조 중 한 단계까지만 복사합니다. 이것을 얕은 복사(shallow copy)라고 합니다.
깊은 복사
반면, 참조 자료형 내부에 중첩되어 있는 모든 참조 자료형을 복사하는 것은 깊은 복사(deep copy)라고 합니다. 그러나 JavaScript 내부적으로는 깊은 복사를 수행할 수 있는 방법이 없습니다. 단, JavaScript의 다른 문법을 응용하면 깊은 복사와 같은 결과물을 만들어 낼 수 있습니다.
JSON.stringify()와 JSON.parse()
JSON.stringify()는 참조 자료형을 문자열 형태로 변환하여 반환하고, JSON.parse()는 문자열의 형태를 객체로 변환하여 반환합니다. 먼저 중첩된 참조 자료형을 JSON.stringify()를 사용하여 문자열의 형태로 변환하고, 반환된 값에 다시 JSON.parse()를 사용하면, 깊은 복사와 같은 결과물을 반환합니다.
const arr = [1, 2, [3, 4]];
const copiedArr = JSON.parse(JSON.stringify(arr));
console.log(arr); // [1, 2, [3, 4]]
console.log(copiedArr); // [1, 2, [3, 4]]
console.log(arr === copiedArr) // false
console.log(arr[2] === copiedArr[2]) // false
간단하게 깊은 복사를 할 수 있는 것처럼 보이지만, 이 방법 또한 깊은 복사가 되지 않는 예외가 존재합니다. 대표적인 예로 중첩된 참조 자료형 중에 함수가 포함되어 있을 경우 위 방법을 사용하면 함수가 null로 바뀌게 됩니다. 따라서 이 방법 또한 완전한 깊은 복사 방법이라고 보기 어렵습니다.
const arr = [1, 2, [3, function(){ console.log('hello world')}]];
const copiedArr = JSON.parse(JSON.stringify(arr));
console.log(arr); // [1, 2, [3, function(){ console.log('hello world')}]]
console.log(copiedArr); // [1, 2, [3, null]]
console.log(arr === copiedArr) // false
console.log(arr[2] === copiedArr[2]) // false
외부 라이브러리 사용
완전한 깊은 복사를 반드시 해야 하는 경우라면, node.js 환경에서 외부 라이브러리인 lodash, 또는 ramda를 설치하면 됩니다. lodash와 ramda는 각각 방법으로 깊은 복사를 구현해 두었습니다. 다음은 lodash의 cloneDeep을 사용한 깊은 복사의 예시입니다.
const lodash = require('lodash');
const arr = [1, 2, [3, 4]];
const copiedArr = lodash.cloneDeep(arr);
console.log(arr); // [1, 2, [3, 4]]
console.log(copiedArr); // [1, 2, [3, 4]]
console.log(arr === copiedArr) // false
console.log(arr[2] === copiedArr[2]) // false
summary
- 배열의 경우 slice() 메서드 또는 spread syntax 등의 방법으로 복사할 수 있다.
- 객체의 경우 Object.assign() 또는 spread syntax 등의 방법으로 복사할 수 있다.
- 위 방법으로 참조 자료형을 복사할 경우, 중첩된 구조 중 한 단계까지만 복사된다. (얕은 복사)
- JavaScript 내부적으로는 중첩된 구조 전체를 복사하는 깊은 복사를 구현할 수 없다. 단, 다른 문법을 응용하여 같은 결과물을 만들 수 있다.
- 대표적인 JSON.stringify()와 JSON.parse()를 사용하는 방법이 있지만, 예외의 케이스가 존재한다. (참조 자료형 내부에 함수가 있는 경우)
- 완전한 깊은 복사를 반드시 해야 하는 경우, node.js 환경에서 외부 라이브러리인 lodash, 또는 ramda를 사용하면 된다.
Chapter2. 스코프
JavaScript에서 스코프(Scope)는 무엇일까요? 게임을 좋아하는 분들이라면 배틀 그라운드와 같은 FPS에 나오는 저격용 스코프가 제일 먼저 생각날 수 있습니다. 이미지가 바로 떠오르지 않는 분은 구글에 ‘스코프’를 검색해 보시면, 총 옆에 붙어 있는 작은 망원경 같은 형태의 물체를 볼 수 있을 겁니다. 스코프는 사격 시 목표물을 정확하게 조준하기 위해 사용합니다. 영어 단어의 뜻 자체도 ‘범위'를 의미하니까, JavaScript에서 이야기하는 스코프 역시 무언가 제한된 범위를 잘 들여다보기 위해 사용되는 개념이라고 추측해 볼 수 있습니다.
컴퓨터 공학, 그리고 JavaScript에서의 스코프는 "변수의 유효범위"로 사용됩니다. 이번 챕터에서는 스코프(Scope)의 종류와 각 선언 키워드 (let, const)를 어떻게 사용해야 하는지 알아봅시다.
학습 목표
- 스코프의 의미와 적용 범위를 이해한다.
- 스코프의 주요 규칙을 이해한다.
- 전역 스코프와 지역 스코프의 차이를 이해한다.
- block scope와 function scope의 차이를 이해한다.
- 변수 선언 키워드(let, const, var)와 스코프와의 관계를 설명할 수 있다.
- 전역 객체가 무엇인지 설명할 수 있다.
Chapter2-1. 스코프와 주요 규칙
- 퀴즈1: 예제를 통해 스코프와 주요 규칙에 대해 생각한다.
- 개념학습: 스코프와 관련된 주요 규칙에 대해 학습한다.
- 퀴즈2: 스코프와 주요 규칙에 대한 이해도를 점검한다.
Chapter2-2. 변수 선언과 스코프
- 개념학습: 스코프 종류와 let, const, var 키워드의 관계에 대하여 학습한다.
- 실습: 개발자도구를 이용하여 학습한 내용을 적용하여 실습한다.
Chapter2-3. 변수 선언할 때 주의할 점
- 개념학습: 변수 선언 시 주의할 점에 대해 학습한다.
- 퀴즈: 변수 선언 시 주의할 점에 대한 이해도를 점검한다.
Chapter3. 클로저
JavaScript에서는 다른 컴퓨터 언어와는 조금 다른 특성을 종종 가지고 있습니다. 그중 종종 사용되는 클로저라는 개념에 대해서 알아보겠습니다. MDN의 클로저 정의에 따르면, 다음과 같습니다.
"함수와 함수가 선언된 어휘적(lexical) 환경의 조합을 말한다. 이 환경은 클로저가 생성된 시점의 유효 범위 내에 있는 모든 지역 변수로 구성된다."
여기서 주목할 만한 키워드는 "함수가 선언"된 "어휘적(lexical) 환경"입니다. 특이하게도 자바스크립트는 함수가 호출되는 환경과 별개로 기존에 선언되어 있던 환경, 즉 어휘적 환경을 기준으로 변수를 조회하려고 합니다. 이와 같은 이유로 "외부 함수의 변수에 접근할 수 있는 내부 함수"를 클로저 함수라고 합니다. 아직 완벽하게 이해되지 않는다고 해도 괜찮습니다. 이번 챕터를 학습하고 나면 여러분의 언어로 클로저를 설명할 수 있게 됩니다.
학습 목표
- 클로저 함수의 정의와 특징에 대해서 이해할 수 있다.
- 클로저가 갖는 스코프 범위를 이해할 수 있다.
- 클로저를 이용해 유용하게 쓰이는 몇 가지 패턴을 이해할 수 있다.
Chapter3-1. 클로저 기초
- 개념학습: 클로저 기초에 대해 학습합니다.
- 퀴즈: 학습한 내용의 이해도를 점검합니다.
개념학습
1️⃣ 클로저는 무엇인가요?
MDN은 클로저를 아래와 같이 정의합니다.
A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). - mdn (2023)
클로저는 함수와 그 함수 주변의 상태의 주소 조합입니다.
조금 더 이해하기 쉽게 풀어서 설명해 보겠습니다.
클로저는 함수와 그 함수가 접근할 수 있는 변수의 조합입니다.
2️⃣ 클로저를 어떻게 구분할 수 있나요?
그렇다면 아래 코드에서는 무엇이 클로저일까요?
const globalVar = '전역 변수';
function outerFn() {
const outerFnVar = 'outer 함수 내의 변수';
const innerFn = function() {
return 'innerFn은 ' + outerFnVar + '와 ' + globalVar + '에 접근할 수 있습니다.';
}
return innerFn;
}
위 코드에 있는 함수부터 찬찬히 살펴보겠습니다.
- 함수 outerFn에서는 변수 globalVar에 접근할 수 있습니다.
- 함수 innerFn에서는 변수 globalVar와 함수 outerFn 내부의 outerFnVar에 접근할 수 있습니다.
즉, 위 코드에서 클로저는 두 조합을 찾을 수 있었습니다.
- 함수 outerFn과 outerFn에서 접근할 수 있는 globalVar
- 함수 innerFn과 innerFn에서 접근할 수 있는 globalVar, outerFnVar
3️⃣ 클로저는 왜 중요한가요?
변수의 접근 범위인 스코프와 비슷한 개념인데, 왜 따로 클로저만 구분을 할까요? 클로저의 함수는 어디에서 호출되느냐와 무관하게 선언된 함수 주변 환경에 따라 접근할 수 있는 변수가 정해지기 때문입니다. 아래 코드를 보며 설명하겠습니다.
innerFnOnGlobal은 outerFn 내부의 innerFn의 주소값을 가집니다. 그다음 줄에서 innerFnOnGlobal을 호출합니다. 이때, innerFnOnGlobal은 innerFn 밖에 있기 때문에 outerFnVar에는 접근하지 못한다고 생각할 수 있는데, 실제 접근할 수 있습니다.
왜 접근할 수 있을까요? innerFn 함수가 최초 선언되었던 환경에서는 outerFnVar에 접근할 수 있기 때문입니다. innerFnOnGlobal은 innerFn의 주소값을 가지고 있고, innerFn은 클로저로서 outerFnVar에 접근할 수 있기 때문입니다. 이 “환경”을 어휘적 환경(Lexical Environment)라고 합니다.
코드를 직접 실행해 봅시다. 실행 결과, 'innerFn은 outer 함수 내의 변수와 전역 변수에 접근할 수 있습니다.'라는 문자열이 리턴되어 message 변수에 담겨 로그로 출력되었습니다. 만약, 클로저가 JavaScript에 없는 개념이라면, outerFnVar에 접근할 수 없어 에러가 났을 겁니다.
디버거에서도 아래와 같이 클로저이기 때문에 접근할 수 있었던 outerFnVar는 따로 분류하고 있는 모습을 확인할 수 있습니다.
실제 클로저를 사용할 때는 outerFn, innerFn처럼 함수가 함수를 리턴하는 패턴을 자주 사용하고, outerFn을 외부 함수, innerFn을 내부 함수라고 통칭합니다. 클로저에 대해 추가 학습 시 “외부 함수의 변수에 접근할 수 있는 내부 함수”등의 표현을 자주 접할 수 있으니 참고 바랍니다.
// 클로저 사용 패턴 1
function outerFn() {
const outerFnVar = 'outer 함수 내의 변수';
const innerFn = function() {
return 'innerFn은 ' + outerFnVar + '에 접근할 수 있습니다.';
}
return innerFn;
}
📝 Summary
- 클로저는 함수와 그 함수 주변의 상태의 주소 조합이다.
- 클로저의 함수는 어디에서 호출되느냐와 무관하게 선언된 함수 주변 환경에 따라 접근할 수 있는 변수가 정해진다.
Chapter3-2. 클로저 활용
- 개념학습: 클로저 활용 방법에 대해 학습합니다.
- 퀴즈: 학습한 내용의 이해도를 점검합니다.
개념학습
클로저의 특징을 활용한 다양한 사례에 대해서 알아봅니다.
데이터를 보존하는 함수
클로저를 활용하면 클로저의 함수 내에 데이터를 보존해 두고 사용할 수 있습니다. 자세히 알아보겠습니다.
일반적으로 함수 내부에 선언한 변수에는 접근할 수 없습니다. 매개변수도 마찬가지입니다.
function getFoodRecipe (foodName) {
let ingredient1, ingredient2;
return `${ingredient1} + ${ingredient2} = ${foodName}!`;
}
console.log(ingredient1); // ReferenceError: ingredient1 is not defined (함수 내부에 선언한 변수에 접근 불가)
console.log(foodName); // ReferenceError: foodName is not defined (매개변수에 접근 불가)
클로저를 응용하면, 함수 내부에 선언한 변수에 접근할 수 있고, 매개변수에도 접근할 수 있습니다. 기존 함수 내부에서 새로운 함수를 리턴하면 클로저로서 활용할 수 있습니다. 즉, 리턴한 새로운 함수의 클로저에 데이터가 보존됩니다.
데이터를 보존하는 함수를 직접 만들어보겠습니다. 레시피를 제작하는 createFoodRecipe 함수를 만들어봅시다. 아래 코드에서는 getFoodRecipe가 클로저로서 foodName, ingredient1, ingredient2에 접근할 수 있습니다. 이때, createFoodRecipe('하이볼')으로 전달된 문자열 '하이볼'은 recipe 함수 호출 시 계속 재사용할 수 있습니다. createFoodRecipe 가 문자열 ‘하이볼’을 “보존”하고 있기 때문입니다.
function createFoodRecipe (foodName) {
let ingredient1 = '탄산수';
let ingredient2 = '위스키';
const getFoodRecipe = function () {
return `${ingredient1} + ${ingredient2} = ${foodName}!`;
}
return getFoodRecipe;
}
const recipe = createFoodRecipe('하이볼');
recipe(); // '탄산수 + 위스키 = 하이볼!'
이를 더 잘 응용하기 위해 getFoodRecipe의 매개변수도 활용할 수 있게 코드를 아래와 같이 변경해 봅시다.
function createFoodRecipe (foodName) {
const getFoodRecipe = function (ingredient1, ingredient2) {
return `${ingredient1} + ${ingredient2} = ${foodName}!`;
}
return getFoodRecipe;
}
const highballRecipe = createFoodRecipe('하이볼');
highballRecipe('콜라', '위스키'); // '콜라 + 위스키 = 하이볼!'
highballRecipe('탄산수', '위스키'); // '탄산수 + 위스키 = 하이볼!'
highballRecipe('토닉워터', '연태고량주'); // '토닉워터 + 연태고량주 = 하이볼!'
highballRecipe 함수는 문자열 ‘하이볼’을 보존하고 있어서 전달인자를 추가로 전달할 필요가 없고, 다양한 하이볼 레시피를 하나의 함수로 제작할 수 있었습니다.
커링
커링은 여러 전달인자를 가진 함수를 함수를 연속적으로 리턴하는 함수로 변경하는 행위입니다. 예시를 먼저 보겠습니다.
sum 함수는 두 전달인자(10, 20)를 덧셈하는 함수고, currySum은 첫 번째 전달인자 10을 리턴하는 함수로 전달해 줍니다. sum과 currySum이 같은 값을 리턴하기 위해서는 currySum 함수에서 리턴한 함수에 두 번째 전달인자 20을 전달하여 호출하면 됩니다. 이렇게 커링을 활용한 currySum과 같은 함수를 커링 함수라고 부르기도 합니다.
function sum(a, b) {
return a + b;
}
function currySum(a) {
const innerSum = function(b) {
return a + b;
};
return innerSum;
}
console.log(sum(10, 20) === currySum(10)(20)) // true
언뜻 봐서는 일반 함수와 커링 함수의 차이가 느껴지지 않지만, 커링은 전체 프로세스의 일정 부분까지만 실행하는 경우 유용합니다. 아래 makePancake 함수는 팬케이크 제작 과정을 커링 함수로 만들었습니다. 팬케이크는 팬케이크 믹스를 만들어두었다가, 나중에 다시 만들 수도 있습니다. 반면, 커링이 적용되지 않은 makePancakeAtOnce 함수는 일부 조리 과정이 생략된 모습을 표현할 수 없습니다.
function makePancake(powder) {
return function (sugar) {
return function (pan) {
return `팬케이크 완성! 재료: ${powder}, ${sugar} 조리도구: ${pan}`;
}
}
}
// 위 함수는 아래 기존 패턴의 함수와 같은 코드입니다.
function makePancake(powder) {
let innerFn1 = function (sugar) {
let innerFn2 = function (pan) {
return `팬케이크 완성! 재료: ${powder}, ${sugar} 조리도구: ${pan}`;
}
return innerFn2;
}
return innerFn1;
}
const addSugar = makePancake('팬케이크가루');
const cookPancake = addSugar('백설탕');
const morningPancake = cookPancake('후라이팬');
// 잠깐 낮잠 자고 일어나서 ...
const lunchPancake = cookPancake('후라이팬');
function makePancakeAtOnce (powder, sugar, pan) {
return `팬케이크 완성! 재료: ${powder}, ${sugar} 조리도구: ${pan}`;
}
const morningPancake = makePancakeAtOnce('팬케이크가루', '백설탕', '후라이팬')
// 잠깐 낮잠 자고 일어나서 만든 팬케이크를 표현할 방법이 없다.
이와 같이 커링은 함수의 일부만 호출하거나, 일부 프로세스가 완료된 상태를 저장하기에 용이합니다.
모듈 패턴
JavaScript에 class 키워드가 없던 시절 모듈 패턴을 구현하기 위해서 클로저를 사용했습니다. 모듈은 하나의 기능을 온전히 수행하기 위한 모든 코드를 가지고 있는 코드 모음으로, 하나의 단위로서 역할을 합니다. 모듈은 다른 모듈에 의존적이지 않고 독립적이어야 합니다.
다른 모듈에 의존적이지 않고 독립적이라면 기능 수행을 위한 모든 기능을 갖추고 있어야 하고, 또한 외부 코드 실행을 통해서 모듈의 속성이 훼손받지 않아야 합니다. 모듈의 속성을 꼭 변경해야 할 필요가 있는 경우에는 제한적으로 노출된 인터페이스에 의해 변경되어야 합니다. 이 특징은 클로저와 유사합니다. 자세히 알아보겠습니다.
아래 코드는 계산기의 최소한의 기능을 모듈 패턴으로 구현했습니다. displayValue는 makeCalculator의 코드 블록 외에 다른 곳에서는 접근이 불가능하지만, cal의 메서드는 모두 클로저의 함수로서 displayValue에 접근할 수 있습니다. 이렇게 데이터를 다른 코드 실행으로부터 보호하는 개념을 정보 은닉(information hiding)이라고 합니다. 이는 캡슐화(encapsulation)의 큰 특징이기도 합니다.
function makeCalculator() {
let displayValue = 0;
return {
add: function(num) {
displayValue = displayValue + num;
},
subtract: function(num) {
displayValue = displayValue - num;
},
multiply: function(num) {
displayValue = displayValue * num;
},
divide: function(num) {
displayValue = displayValue / num;
},
reset: function() {
displayValue = 0;
},
display: function() {
return displayValue
}
}
}
const cal = makeCalculator();
cal.display(); // 0
cal.add(1);
cal.display(); // 1
console.log(displayValue) // ReferenceError: displayValue is not defined
이와 같이 클로저는 특정 데이터를 다른 코드의 실행으로부터 보호해야 할 때 용이합니다.
📝 Summary
- 클로저는 주로 데이터를 보존하는 함수, 커링, 모듈 패턴으로 활용한다.
- 클로저를 이용하면 특정 함수가 데이터를 보존할 수 있다.
- 커링을 이용하면 함수의 일부만 호출하거나, 일부 프로세스가 완료된 상태를 저장할 수 있다.
- 모듈 패턴을 이용하면 특정 데이터를 다른 코드의 실행으로부터 보호할 수 있다.