13일차

JiHun·2023년 4월 27일

부트캠프

목록 보기
13/56

JavaScript 핵심 개념과 주요 문법

원시 자료형과 참조 자료형

자료형(type)이란 값(value)의 종류다. 각각의 자료형은 고유한 속성과 메서드를 가지고 있다. 자료형은 크게 두 가지로 구분할 수 있는데, 바로 원시 자료형과 참조 자료형이다.

// 원시 자료형(primitive type): number, string, boolean, undefined, null
10, '문자열', true, undefined, null

원시 자료형이 아닌 모든 자료형은 참조 자료형이다. 여러 데이터를 한 번에 다룰 수 있는 배열, 객체가 대표적인 참조 자료형이다. 함수도 참조 자료형으로 분류.

// 참조 자료형(reference type)
[0, 1, 2]   // 배열
{ name: "Gil-Dong, Hong", age: 18 } // 객체
function add(x, y) { return x + y } // 함수

원시 자료형의 특징
1. 원시 자료형을 변수에 할당하면 메모리 공간에 값 자체가 저장된다.
2. 원시 값을 갖는 변수를 다른 변수에 할당하면 원시 값 자체가 복사되어 전달된다.
3. 원시 자료형은 변경 불가능한 값(immutable value). 즉, 한 번 생성된 원시 자료형은 읽기 전용(read only) 값이다.

참조 자료형의 특징
1. 참조 자료형을 변수에 할당하면 메모리 공간에 주소값이 저장된다.
2. 참조 값을 갖는 변수를 다른 변수에 할당 하면 주소값이 복사되어 전달된다.
3. 참조 자료형은 변경 가능한 값(mutalbe value).

왜 다르게 분리를 해놨을까?

  • 원시자료형: 변수 선언을 하게 되면 공간을 확보하고 변수를 할당하면 그 공간에 저장한다.
  • 참조 자료형: 배열의 요소가 각각의 하나의 값이기 때문에 하나의 공간에 배열 자체를 저장하는 것이 불가능하다. 만약 배열의 요소를 하나의 공간에 부여하게 되면, 추가 및 삭제가 수시로 일어나기 때문에 원하는 데이터의 조회가 어려워진다.

참조 자료형은 특별한 저장 공간에 저장한 후, 그 저장공간을 참조할 수 있는 주소값을 변수에 저장한다. 즉, 변수에 해당하는 저장공간에는 주소값이 저장되어 있고 주소값을 통해 참조 자료형에 접근할 수 있다.

둘의 큰 차이점이 있다.

원시 자료형은 원본에 다른 값을 재할당해도 복사본에는 영향을 미치지 않는다. 하지만 참조 자료형은 원본을 변경하면 복사본도 영향을 받는다. 왜냐하면 값은 주소를 참조하고 있기 때문이다.

원시 자료형에 다른 변수를 재할당하면 무슨 일이 일어날까?

재할당하게 되면 원시 값을 저장하기 위해 새로운 공간을 확보한 뒤, 그 공간에 새롭게 저장한다.
남아있는 값은 JavaScript 엔진이 알아서 사용하지 않는 값은 삭제한다. 결국 변수는 변경하는게 아니라 읽기 전용이어서 새로운 값으로 바꾼다는 말이 더 맞는거 같다. 재할당으로 변경할 수 없고 복사만 가능하기 때문에 원시 자료형 값의 신뢰성이 높다.

참조 자료형을 변경하려면?

변수에는 참조 자료형이 있는 저장공간(heap)의 주소값을 저장하고 있다. 원시 자료형의 경우 값의 크기가 거의 일정해서 새로운 공간을 확보하여 값을 복사하는 방법은 유용하지만, 크기가 얼마나 커질지 모르는 참조 자료형의 경우 매번 값을 복사한다면 효율성을 떨어질 수 밖에 없다. 이런 이유로 참조 자료형은 변경 가능하도록 설계되어있다.

그러면 원시 자료형은 배열처럼 인덱스를 이용해서 변경이 안되나?

정답은 안된다. 문자열도 원시 자료형이기 때문에 값을 변경할 수 없다.


얕은 복사와 깊은 복사

  • 원시 자료형이 할당된 변수를 다른 변수에 할당하면 값 자체의 복사가 일어난다. 따라서 원본과 복사본 중 하나를 변경해도 다른 하나에 영향을 미치지 않는다.
  • 참조 자료형이 할당된 변수를 다른 변수에 할당하면 주소가 복사되어 원본과 복사본 중 하나를 변경해도 다른 하나에 영향을 미치지 않는다.
  • 참조 자료형의 주소값을 복사한 변수에 요소를 추가하면 같은 주소를 참조하고 있는 원본에도 영향을 미친다.
  • 참조 자료형이 저장된 변수를 다른 변수에 할당할 경우, 두 변수는 같은 주소를 참조하고 있을 뿐 값 자체가 복사되었다고 볼 수 없다.

그러면 배열을 어떻게 복사할까?

배열을 복사하는 방법은 크게 두 가지.

1. slice()
let arr = [1,2,3]
let copiedArr = arr.slice();
console.log(copiedArr); // [1,2,3]
console.log(arr === copiedArr); // false

slice()로 복사하면 참조하는 주소값이 다르다.

2. 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(arr === copiedArr);  // false

이제는 객체 복사하기

1. Object.assign()

let obj = { firstName: "Gil-Dong", lastName: "Hong" };
let copiedObj = Object.assign({}, obj);

console.log(obj === copiedObj) // false

2. spread syntax

배열뿐만 아니라 객체를 복사할 때도 사용할 수 있다.

let obj = { firstName: "Gil-Dong", lastName: "Hong" }
let copiedObj = {...obj};

console.log(obj === copiedObj)  // false

얕은 복사

예외
참조 자료형 내부에 참조 자료형이 중첩되어 있는 경우, slice(), Object.assign(), spread syntax를 사용해도 참조 자료형 내부에 참조 자료형이 중첩된 구조는 복사할 수 없다. 참조 자료형이 몇 단계로 중첩되어 있던지, 한단계까지만 복사 할 수 있다.

이 경우에는 제일 상위 참조 자료형을 복사하면 서로 다른 주소를 가지지만, 내부에 있는 참조 자료형들은 같은 주소를 쓰고 있기 때문에 서로 비교 하면 true가 나온다.

let users = [
	{
		name: "kimcoding",
		age: 26,
		job: "student"
	},
	{
		name: "parkhacker",
		age: 29,
		job: "web designer"
	},
];

let copiedUsers = users.slice();
console.log(users === copiedUsers);  // false
console.log(users[0] === copiedUsers[0]);  // true

slice(), Object.assign(), spread syntax등의 방법으로 참조 자료형을 복사하면 , 중첨된 구조 중 한 단계까지만 복사한다. 이것을 얕은 복사(shallow copy) 라고 한다.

깊은 복사

반면, 참조 자료형 내부에 중첩되어 있는 모든 참조 자료형을 복사하는 것은 깊은 복사(deep copy) 라고 한다. 그러나 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 === 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, ramdafmㄹ 쓰면 된다.

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

스코프(Scope)

변수에 접근할 수 있는 범위가 존재 한다. 중괄호 안에 변수가 선언되었는가, 바깥쪽에 변수가 선언되었는가? 이 범위를 스코프라고 한다.

스코프의 규칙

첫번째 규칙

let greeting = '반갑소';
function greetSomeone() {
	let firstName = '길동';
	return greeting + ' ' + firstName;
}

console.log(greetSomeone());  // '반갑소 길동'
console.log(firstName);       // ReferenceError

스코프를 중괄호 안과 밖으로 나눌 수가 있다. greeting 변수는 바깥에 정의되어 있어서 중괄호 안 스코프에서도 사용할 수 있다. 하지만 firstName은 중괄호 안에 정의되어 있으므로 바깥에서는 접근이 불가능하다. 즉, 안쪽 스코프에서 바깥쪽 스코프로는 접근할 수 있지만 반대는 불가능.

두번째 규칙

스코프는 중첩이 가능하다. 그리고 가장 바깥쪽의 스코프는 전역 스코프(Global Scope)라고 부르고, 반대는 지역 스코프(Local scope)라고 한다. 또, 지역 변수는 전역 변수보다 더 높은 우선 순위를 가진다.

let name = '홍길동';
function showName() {
	let name = '아버지';
	console.log(name);
}


console.log(name);  // 홍길동
showName();         // 아버지
console.log(name);  // 홍길동

전역 스코프에서 정의된 변수명과 함수 안에 있는 변수명과 같지만, 지역 스코프 안에 있는 변수에 우선순위가 매겨진다.

let name = '홍길동';
function showName() {
	name = '아버지';   // < == let 선언을 뺌.
	console.log(name);
}


console.log(name);  // 홍길동
showName();         // 아버지
console.log(name);  // 아버지

위 코드랑 다른 점은 let 선언을 하지 않았다 이렇게 되면 함수 안 스코프에서 전역 변수에 있는 변수를 재할당하는 효과를 가진다. 따라서 전역 스코프에 있는 변수가 변경된다. 지역 스코프에서 새로 선언되지 않으면 그냥 같은 변수다.

변수 선언과 스코프

블록 스코프

중괄호로 둘러싼 범위

if(false) {}
for () {}
{}

함수 스코프

function getName() {
	return user.name;
}

let getAge = function() {
	return user.age;
}

함수 선언식 및 함수 표현식은 함수 스코프를 만든다.
유의할 점. 화살표 함수는 블록 스코프로 취급된다. 함수 스코프가 아니다.

함수 스코프와 블록 스코프는 논리적인 구분 외에도 코드를 작성할 때 다른 점이 몇가지 존재 한다.

블록 스코프와 var 키워드

for (var i = 0; i < 5; i++) {
	console.log(i); 
}

console.log('final i:', i); // 5

let으로 i를 선언했다면, 블록 스코프 안에서만 쓰이는 i가 var 키워드랑 만나면 for문이 만들어낸 블록 스코프를 무시한다.

var 키워드와 let 키워드

var 키워드로 정의한 변수는 블록 스코프를 무시하고, 함수 스코프만 따른다. 그러나, 모든 블록 스코프를 무시하는게 아니라 화살표 함수의 블록 스코프는 무시하지 않는다.

함수 스코프는 함수의 실행부터 종료까지이고, var 선언은 함수 스코프의 최상단에 선언된다. 선언 키워드 없는 선언은 최고 스코프에 선언된다.

let 키워드는 재선언을 방지한다. 따라서 var 키워드보다 let 키워드가 안전하다.

const 키워드

  • let 키워드와 동일하게, 블록 스코프를 따른다.
  • 값의 변경을 최소화 한다. 값을 새롭게 할당할 일이 없다면, const 키워드를 사용해라.
  • 값을 재할당 하는 경우, 에러.

클로저

자바스크립트는 함수가 호출되는 환경과 별개로 기존에 선언되어 있던 환경, 즉 어휘적 환경을 기준으로 변수를 조회하려고 한다. 외부 함수의 변수에 접근할 수 있는 내부함수를 클로저 함수라고 한다.

클로저 기초

클로저의 함수는 어디에서 호출되느냐와 무관하게 선언된 함수 주변 환경에 따라 접근할 수 있는 변수가 정해진다.

const globalVar = '전역 변수'
function outerFn() {
	const outerFnVar = 'outer 함수 내의 변수';
  	const innerFn = function() {
    	return 'innerFn은 ' + outerFnVar + '와 ' + globalVar + '에 접근할 수 있습니다.';
    }
    return innerFn
}

const innerFnOnGlobal = outerFn();
const message = innerFnOnGlobal();

outerFn은 함수다. 함수는 참조 자료형. 함수는 변수에 넣을 수도 있다.
innerFnOnGlobalouterFn()와 같지는 않지만 참조 자료형을 복사한다.
또한, innerFn를 return 하기 때문에 innerFn의 클로저인 globalVar 변수와 outerFnVar 변수에 접근할 수 있다.

클로저 활용

클로저를 응용하면, 함수 내부에 선언한 변수에 접근할 수도 있고, 매개변수에도 접근할 수 있다. 기존 함수 내부에서 새로운 함수를 리턴하면 클로저로서 활용할 수 있다. 즉, 리턴한 새로운 함수의 클로저에 데이터가 보존된다.

데이터를 보존하는 함수

클로저를 활용하면 클로저의 함수 내에 데이터를 보존해 두고 사용할 수 있다. 매개변수도 마찬가지.

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 (매개변수에 접근 불가)

이렇게 만들면 함수 내부 변수에 접근할 수 없지만

function createFoodRecipe (foodName) {
  const getFoodRecipe = function (ingredient1, ingredient2) {
    return `${ingredient1} + ${ingredient2} = ${foodName}!`;
  }
  return getFoodRecipe;
}

const highballRecipe = createFoodRecipe('하이볼');
highballRecipe('콜라', '위스키'); // '콜라 + 위스키 = 하이볼!'
highballRecipe('탄산수', '위스키'); // '탄산수 + 위스키 = 하이볼!'
highballRecipe('토닉워터', '연태고량주'); // '토닉워터 + 연태고량주 = 하이볼!'

getFoodRecipe가 클로저로서 foodName, ingredient1,ingredient2에 접근할 수 있고, createFoodRecipe('하이볼')으로 전달된 문자열 '하이볼'highballRecipe() 함수 호출 시 계속 재사용할 수 있다. createFoodRecipe가 문자열 '하이볼'을 "보존"하고 있기 때문이다.

커링

function makePancake(powder) {
  return function (sugar) {
		return function (pan) {
			return `팬케이크 완성! 재료: ${powder}, ${sugar} 조리도구: ${pan}`;
		}
	}
}

const addSugar = makePancake('팬케이크가루');
const cookPancake = addSugar('백설탕');
const morningPancake = cookPancake('후라이팬');

// 잠깐 낮잠 자고 일어나서 ...
const lunchPancake = cookPancake('후라이팬');
function makePancakeAtOnce (powder, sugar, pan) {
  return `팬케이크 완성! 재료: ${powder}, ${sugar} 조리도구: ${pan}`;
}

const morningPancake = makePancakeAtOnce('팬케이크가루', '백설탕', '후라이팬')
// 잠깐 낮잠 자고 일어나서 만든 팬케이크를 표현할 방법이 없다.

커링은 함수의 일부만 호출하거나, 일부 프로세스가 완료된 상태를 저장하기에 용이하다.

모듈 패턴

class 키워드가 없던 시절 모듈 패턴을 구현하기 위해 클로저를 사용했다.

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

클로저는 특정 데이터를 다른 코드의 실행으로부터 보호해야 할 때 용이하다.
왜 용이하나면 makeCalculator안에 있는 displayValue

마지막으로

함수는 참조 자료형이고 변수에 객체 형태로 복사 할수 있다. 그 객체 안에 있는 프로퍼티들이 함수를 리턴하는 것이 클로저를 활용한 패턴이다. 즉, 함수를 리턴 객체 형태로 변수에 넣어놓고, 그 변수가 리턴을 사용할 수 있는 함수 형태를 가지게 한다는 말 같다. 이렇게 쓰면서도 무슨말인지 모르겠다. 다른건 몰라도 클로저는 계속 반복 공부해야겠다. 뭔가 class 형태랑 비슷해 보인다. class와 비교 공부 해봐야겠다.

profile
안녕하세요. 프론트엔드 개발자 송지훈입니다.

0개의 댓글