[TIL] JavaScript 핵심 개념과 주요 문법 part.1

lmimoh·2022년 9월 6일
0

TIL

목록 보기
9/26
post-thumbnail

원시자료형의 정의

JavaScript에서 원시자료형의 데이터는 다음과 같다.

  • String
  • Number
  • Bigint
  • Boolean
  • undefined
  • Symbol
  • Null

모든 원시자료형은 _단수의 정보 를 가지고 있다. 이는 당시의 메모리 용량이 매우 한정적이었기에 변수에 데이터 용량이 제한된 하나의 데이터만 담을 수 있었기 때문이다.

이때, 원시자료형은 항상 '하나' 의 데이터를 가지므로 메모리의 크기를 고정하는 것이 효율적 일 수 있었다.(메모리의 최대 크기를 예측할 수 있기 때문에)

따라서, 원시자료형의 경우 크기 여부와 상관없이 하나의 데이터만을 저장소에 담을 수 있으며 메모리 내 값 자체에 대한 변경은 불가능하지만 변수에 다른 데이터를 할당하는 것은 가능 하다.

'data_'  // 메모리 내 data_ 생성
'data__' // 메모리 내 data__ 생성

let value = 'data_';  // 변수 value에 data_ 할당
let value = 'data__'; // 변수 value에 data__ 재할당, 이때 data_ 이 data__로 변경되는 것은 아님

참조자료형의 정의

단수개의 데이터를 다루는 원시자료형을 제외한 모든 타입이 참조자료형에 해당된다.

과거에는 'List'라는 개념을 구현하는 것이 어려워 띄어쓰기, 탭, 쉼표 등으로 데이터를 구분하는 자료 구조를 주로 사용했다. 이는 'csv'라는 파일에서 확인해볼 수 있다.

이처럼 split 방식으로 데이터를 구분하여 다루던 과거에서 오늘날 대부분의 프로그래밍 언어에 배열과 비슷한 자료구조가 생겨났다. 이는 어떤 이유일까?

만약, 어떤 문자열의 길이가 100이상이라고 가정해보자. 원시자료형의 고정된 메모리 크기를 사용할 경우, 위의 문자열을 가진 배열의 요소는 34개 이상이 될 수가 없다. 즉, 34개 이상의 해당 문자열 요소를 지니게 되면 새로운 배열을 만들어야 한다. 이는 '데이터 크기에 따라 동적으로 변하는 저장소' 의 필요성을 시사한다.

이제 우리가 참조자료형을 사용하는 경우, 변수는 콜 스택(Call Stack) 에 생성되고 힙 메모리(Heap Memory) 에 저장되는 데이터의 주소를 참조하게 된다.

let a = {
  name : 'mimoh',
  gender : 'M',
  age : 24
};

// 변수 a는 콜 스택에 생성, 객체의 내용인 { name : 'mimoh', gender : 'M', age : 24 }는
// 힙 메모리에 생성되고 해당 주소가 변수 a의 내용에 담기게 된다.

데이터를 복사하는 경우

이러한 원시형자료와 참조형자료의 특징은 변수를 복사한 경우 에 두드러진다.

원시형자료의 경우 변수를 복사했을 때 변수를 새로 생성하고 값 자체를 넘겨주는 것이므로, 새로 생성한 변수에 값을 재할당하더라도 원본에는 영향을 끼치지 않는다.

하지만, 참조자료형의 경우 변수를 복사했을 때 참조된 주소를 넘겨주는 것이므로, 새로 생성한 변수에 변경점이 생기게 된 경우 원본에 영향을 끼치게 된다.

let a = 'value'; 
let b = a;      // b = 'value'
b = 'value_';   // a = 'value', b = 'value_'

let c = { a : 1, b : 2, c : 3 };
let d = c; 
delete d.b; // c = { a : 1, c : 3 }, d = { a : 1 , c : 3 }

스코프(Scope)란?

스코프는 변수의 유효 범위 혹은 접근 규칙 을 의미한다.

변수에는 접근할 수 있는 범위가 존재하는데, 이는 변수의 선언 위치가 중괄호(블록) 내부인지 외부인지에 따라 결정된다.

스코프의 규칙은 다음과 같다.

  • 내부 스코프에서 외부 스코프에 존재하는 변수에 접근 이 가능하다. but, 반대는 불가능하다.

  • 스코프는 중첩이 가능한 형태 이다.
    > 이때 가장 바깥쪽의 스코프를 전역 스코프 , 전역이 아닌 모든 스코프를 지역 스코프 라고 부른다. 또한, 해당 스코프에서 선언된 변수를 전역변수지역변수 라고 부른다.

  • 지역변수는 전역변수보다 높은 우선순위를 가진다.


블록스코프와 함수스코프

블록 스코프는 함수로 선언된 중괄호 블록과 전역 스코프를 제외한 모든 스코프 를 의미한다. 반대로, 함수 스코프는 함수로 선언된 중괄호 블록 을 의미한다.

이때, 화살표 함수는 블록스코프 로 취급한다.

let a = 1; // 전역 스코프

if(a === 1) {
  let b = 2 ;
} // 블록 스코프

function getNum () {
  return 0;
} // 함수 스코프

const c = (arg1, arg2) => a + b; // 블록 스코프

var 키워드

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

함수 스코프는 함수의 실행부터 종료까지이며, var 선언은 위치와 관계 없이 함수의 최상단에 선언 된다(호이스팅). 선언 키워드가 존재하지 않는 변수의 경우에도 var 변수처럼 취급한다.

이때, 보통 코드 작성은 블록 단위의 들여쓰기를 적용하고 그 구분이 시각적으로 분명하다. 따라서 개발자는 블록 스코프를 기준으로 작성하고 생각하게 된다.

그러나, var 키워드로 선언된 변수는 해당 규칙을 무시하기 때문에 해당 스코프에 대한 이해가 없는 경우 코드의 혼란을 발생시킬 수 있다.

또한, var 키워드는 변수의 재선언이 가능하다.

var let const
변수범위 함수스코프 블록스코프
함수스코프
블록스코프
함수스코프
재선언 가능 불가능 불가능
재할당 가능 가능 불가능

변수선언 시 주의점

브라우저에는 window라는 객체 가 존재한다. 이는 브라우저의 창을 의미하는 객체 이지만, 전역 변수와 전역 함수에 대한 내용 도 담고 있다. 즉, 전역으로 선언된 변수 혹은 함수는 window 객체에 포함되게 된다.

그렇다면, 전역변수는 좋은 것일까?

사실, 필요에 의한 전역변수가 아닌 경우 사용을 최소화 하는 것이 좋다.

어플리케이션의 개발은 여러 명의 개발자가 동시에 참여하게 되고 본인이 작성하지 않은 로직 혹은 함수가 대량으로 포함되게 된다. 이때, 무분별하게 생성된 전역변수는 전체 코드에 혼란을 줄 수 있고 이를 side effect 라고 부른다.

또한, 기본 매소드 혹은 객체의 이름으로 전역변수를 선언한 경우 window 객체에 존재하는 내장 기능을 덮어서 사용하지 못하게 될 수도 있다.

var console = {};
console = {}; // 키워드가 없는 변수 선언은 var로 선언된 전역변수처럼 작동한다.

console.log('Hello, World'); // 에러 발생

이를 방지하기 위해 'use strict';를 사용할 수 있으며, 이는 브라우저에서 JavaScript가 보다 엄격하게 작동하게 한다.


클로저 함수란?

'함수와 함수가 선언된 어휘적(lexical) 환경의 조합을 말한다. 이 환경은 클로저가 생성된 시점의 유효 범위 내에 있는 모든 지역 변수로 구성된다.'

쉽게 설명하자면, 클로저 함수는 '함수를 리턴하는 함수' 의 형태를 가지고 있다.

이때, 클로저 함수는 복수개의 함수로 이루어져 있기에 내부/외부로 나뉘어질 수 있다. 또한, 스코프의 개념에 따라 내부의 함수는 외부 함수의 변수에 접근할 수 있지만 반대의 경우는 불가능하다.

클로저 함수의 특징은 다음과 같다.

  • 외부 함수의 인자를 유지 할 수 있다.
    > 클로저는 특정 데이터를 외부 함수 스코프 내에 가두어 둔 채로 사용할 수 있게 해준다.

  • 클로저를 통해 불필요한 전역변수 사용을 줄이고, 외부 스코프에서 값에 대한 접근을 제한 할 수 있다.(캡슐화)

  • 외부 함수에 의해 리턴된 객체는 내부 함수를 통해 생성된 값을 독립적으로 가지게 된다. 따라서, 외부 함수를 통해 두개의 객체를 생성한 경우 해당 객체는 서로 영향을 주지 않고 각각의 값을 보존한다.(모듈화)


클로저 함수의 활용

1) 외부 함수의 인자를 유지할 수 있다.

const addNum = function (x) { // 외부함수
  return function (y) {       // 내부함수
    return x + y;
  }
}

const getResult = addNum(5);
// addNum의 결과는 함수이므로 내부 함수에 전달인자 x가 유지된 채로 값이 담겨 있다.

getResult(4); // 5 + 4 = 9; 
getResult(10); // 5 + 10 = 15; 
// 또한, 외부 함수에서 내부 함수의 변수에 접근할 수 없으므로 내부 함수의 로직 혹은 변수가 예상치
// 못한 상황에서 변경되거나 변조되는 것을 방지할 수 있다.

2) 클로저 함수의 캡슐화

const makeCounter = () => {
  let result = 0;
  
  return {
    increase : () => {
      value++;
    },
    
    decrease : () => {
      value--;
    },
    
    getResult : () => result
  }
}

const counter_ = makeCounter();
counter_; //{increase : ..., decrease : ..., getResult : ... }의 객체를 참조

makeCounter 함수는 increase, decrease, getResult 함수를 포함한 객체 하나를 반환한다. 이때, makeCounter 함수의 내용을 변경하지 않고서는 내부에 존재하는 result 변수를 변경할 수 없다.

(객체의 메소드를 이용하는 경우 간접적으로 접근 가능)

이처럼, side effect를 최소화하기 위해 클로저 함수 내부에 변수를 선언하는 경우 makeCounter를 통해 만들어지는 객체 내에서 result를 전역변수처럼 사용할 수 있는 한편, 스코프를 통해 변수를 보호 할 수 있다.

이를 정보의 접근 제한(캡슐화) 라고 부른다.

3) 클로저 함수의 모듈화

const counter_ = makeCounter();
counter_.increase;
counter_.increase;
counter_.increase;
counter_.getResult; // result = 3;

const counter__ = makeCounter();
counter__.increase;
counter__.increase;
counter__.decrease;
counter__.getResult; // result = 2;

함수 makeCounter를 통해 두 개의 객체를 생성했을 때, 각 객체의 result 변수의 값은 독립적으로 존재하게 된다. 이는 makeCounter라는 함수가 재사용이 가능함을 의미한다.

즉, 함수를 통해 생성되는 객체가 독립적 으로 사용될 수 있을 때 우리는 함수를 완전히 독립적인 부품 형태 라고 정의할 수 있고 이를 모듈화라고 한다.

클로저 함수는 그 특징을 통해 함수 내 데이터와 매서드를 묶어서 객체의 형태로 독립적으로 다룰 수 있기 때문에 모듈화에 유리 하다.


profile
성장하는 개발자, 이민훈입니다.

0개의 댓글