TIL 13일차 - [JavaScript] 자료형, 스코프, 클로저

Yoon Kyung Park·2023년 4월 26일
0

TIL

목록 보기
13/75
  • 원시 자료형(primitive data type)과 참조 자료형(reference data type)의 구분이 왜 필요한지 이해할 수 있다.

    o
    원시 자료형은 number, string, boolean, undefined, null, symbol의 값을 의미하며, 참조 자료형은 배열, 객체, 함수, 원시 자료형이 아닌 타입을 의미한다.
    이 두 자료형은 값을 저장하고 변경하는 데에 있어 차이가 있기 때문에 구분하여 사용해야한다.

  • 원시 자료형과 참조 자료형의 차이를 이해하고, 각자 맞는 상황에서 사용할 수 있다.

    o
    원시 자료형은 값의 크기가 일정하여 새로운 공간에 값을 저장하는 것이 용이하다.
    그래서 원본의 공간이 아닌 별도의 공간을 새로 만들어 원본과 동일한 값을 저장한다.
    그렇기에 원본의 값 자체를 저장한다.
    이때 원본의 값은 처음 저장된 원본의 값을 저장하기 때문에
    이미 복사하여 저장된 복사본에서 값을 변경한다 해도 원본의 값에는 영향을 미치지 않으며,
    마찬가지로 원본의 값을 바꿔도 복사본의 값에는 영향을 주지 않는다.

    예를 들면, 서류 원본을 복사기에 복사하면, 처음 서류 원본에 적힌 글자들(원시값)이 복사된다.
    이때 복사본을 찢어도 원본의 값은 변하지 않으며,
    원본의 값을 지우고 추가해도 이미 복사된 복사본의 값에 영향을 주지 않는다.

    따라서 원시 자료형은 값 자체를 저장하며, 원시값 자체를 복사하므로 변경이 불가한 값이다.

    참조 자료형은 값의 크기가 일정하지 않으므로 매번 새로운 공간을 확보하여 값을 복사하여 저장하면 많은 공간이 필요하고, 매번 변경할 때마다 저장된 공간들을 찾아가 일일이 변경해야 한다.
    이는 매우 비효율적이기 때문에 이러한 과정을 생략하고자 참조 자료형이 등장했다.
    참조 자료형은 원본의 값을 저장하는 것이 아니라 원본의 값이 저장된 공간에 대한 주소가 저장되는 것이다. 그렇기 때문에 주소만 복사된 것이라 값이 저장된 공간은 여전히 동일하다.
    따라서 복사된 주소를 따라가 그 공간에서 값을 변경하면 원본의 값도 변하게 되는 것이다.

    예를 들면, 보물이 들어있는 보물상자가 있다고 하자.
    매번 보물상자에 담긴 수많은 보석들을 일일이 한 공간에 하나씩 담으려 하니
    시간도 공간도 너무 많이 차지하고,
    원하는 보석을 사용하거나 팔거나 추가하려고 할 때마다
    그 보석이 담겨있는 곳으로 가야하는 번거로움이 생겨 비효율적이라고 생각이 들었다.
    그래서 보물상자를 한 곳에 담아두고 이 보물상자를 필요할 때마다
    찾아가 사용할 수 있도록 지도를 만들었다.
    그리고 언제든 이 지도를 따라 가면 보물상자가 있도록 했다.
    지도는 여러장 복사할 수 있다.
    그런데 만약 누군가 그 지도를 가지고
    보물상자가 있는 곳에 가서 보물을 몇 개 가져왔다고 해보자.
    그러면 보물상자에 있는 보물들은 누군가가 가져가기 전과 같은 보물상자일까?
    아니다. 이미 보물상자는 누군가가 가져간 순간부터 그 안의 값은 변경된 것이다.

    자, 보물이 들어있는 보물상자는 참조 자료형의 값이 들어 있는 공간이다. 이를 heap이라고 한다. 이 보물상자를 찾아가기 위한 지도는 참조 자료형의 주소다. 참조 자료형은 보물을 복사하는 게 아니라 이 지도를 복사하는 것이다. 그래서 만약 값을 변경하면 원본의 값도 변경되는 것이다.

    따라서 참조 자료형은 주소값을 저장하며, 원시값이 아닌 주소값을 복사하므로 변경이 가능한 값이다.

  • 원시 자료형이 할당될 때는 변수에 값(value) 자체가 담기고, 참조 자료형이 할당될 때는 보관함의 주소(reference)가 담긴다는 개념을 코드로 설명할 수 있다.

    o
    1) 원시 자료형이 할당될 때는 변수에 값 자체가 담긴다.

    let num = 31;
    let copiedNum = num;
    console.log(copiedNum); // 31
    console.log(num === copiedNum); // true

    그러면 원본의 값을 바꿔보자. 어떻게 달라지는지 변화를 보자.

    let num = 31;
    let copiedNum = num;
    num = 21;
    console.log(num); // 21;
    console.log(copiedNum); // 31;
    console,log(num === copiedNum); // false

    이처럼 원본을 복사하면 원본의 맨 처음값이 복사되어
    원본이 바뀌어도 복사본의 값에는 영향을 주지 않는다.
    반대로 복사본의 값이 변경되어도 원본의 값은 변하지 않는다.

    2) 참조 자료형이 할당될 때는 보관함의 주소가 담긴다.

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

    그러면 이번엔 복사본의 값을 바꿔보자. 어떻게 달라지는지 변화를 보자.

    copiedArr[2] = 5; // 5
    console.log(copiedArr); // [0,1,5,3]
    console.log(arr); // [0,1,5,3]
    console.log(arr === copiedArr); // true

    이처럼 복사본의 값을 변경하면, 원본의 값도 변한다.
    이는 원본의 값이 들어있는 공간의 주소를 복사한 것이기 때문에
    원본과 복사본은 다른 주소지를 가지고 있을 뿐, 같은 공간을 공유하고 있는 것이다.
    그렇기에 같은 공간에서 변경된 값은 서로의 값에 영향을 준다.

  • 참조 자료형은 기존에 고정된 크기의 보관함이 아니라, 동적으로 크기가 변하는 특별한 보관함을 사용한다는 것을 이해할 수 있다.

    o
    참조 자료형은 배열, 객체, 함수 등과 같이 일정한 크기의 값이 아니기 때문에 한 공간에 모든 값을 담기에는 비효율적이다. 이를 보완하기 위해 참조 자료형이 생긴 것이다.

  • 참조 자료형인 값을 복사하는 방법에 대해서 이해한다.

    o
    참조 자료형은 값 자체를 복사하는 것이 아닌 값이 저장된 공간에 대한 주소를 복사하는 것이다.
    예를 들어, 나와 동생은 같은 사물함을 공유하고 있다.
    그 사물함으로 가는 네비게이션 경로를 복사하여 동생에게 공유했다.
    동생이 사물함에 가서 사물함에 책을 추가했다고 하자.
    내가 다음날 사물함을 열면 사물함에는 동생이 두고 간 책이 추가된 것이다.
    이처럼 참조 자료형은 사물함의 책을 복사한 게 아니라
    책이 담긴 사물함으로 가는 주소를 복사한 것이다.
    따라서 사물함 안의 내용물이 바뀌면,
    나의 사물함이자 동생의 사물함의 내용물도 바뀌게 되는 것이다.

  • 스코프의 의미와 적용 범위를 이해한다.

    o
    scope는 변수가 어디까지 영향을 미칠 수 있는지에 대한 개념이다.
    스코프는 범위에 대한 개념이기에 이 범위가 어디까지인지 구분할 기준이 2가지가 있다.
    중괄호{}를 기준으로 구분되는 블록스코프(block scope)와
    함수 function 키워드를 기준으로 구분되는 함수스코프(function scope)가 있다.
    이때, 블록스코프에는 함수이지만 function 키워드가 없는 화살표 함수가 포함되며,
    함수스코프에는 함수선언식과 함수표현식 모두 포함된다.

    스코프를 구분하는 또 다른 기준은 변수와 접근 가능 여부다.
    블록/함수 스코프를 기준으로 구분된 범위 내에서
    바깥 변수는 안쪽에서 가져와 사용할 수 있으나,
    그 범위 밖에서는 안쪽 변수를 가져와 사용할 수 없다.
    구분된 하나의 범위 내에서 선언된 변수는 그 범위 내에서만 사용가능하다.

    블록/함수 스코프를 기준을 구분된 범위 내에서
    가장 바깥쪽은 전역스코프(Global scope)이고,
    안쪽의 나머지는 지역스코프(local scope)다.
    바깥쪽에서 안쪽으로 좁혀 접근하는 것은 불가능하나,
    안쪽에서 바깥쪽으로 넓혀 접근하는 것은 가능하다.
    이는 결국, 안쪽에서 바깥쪽까지 접근 할 수 있으므로
    바깥으로 접근하여 바깥 변수를 가져와 안쪽에서 사용할 수 있으며,
    바깥쪽에서 안쪽으로는 좁혀 접근하는 것이 불가능하므로
    안쪽 변수를 바깥으로 가져가 사용할 수 없다는 것과 같은 의미다.

  • 스코프의 주요 규칙을 이해한다.

    o
    안쪽 범위인 지역 스코프에서 선언한 변수는 지역변수라 하며,
    가장 바깥 범위인 전역 스코프에서 선언한 변수는 전역변수라 한다.
    이때, 지역변수는 전역변수보다 우선 순위가 높아 먼저 사용된다.

  • 전역 스코프와 지역 스코프의 차이를 이해한다.

    o (위에서 설명함)

  • block scope와 function scope의 차이를 이해한다.

    o (위에서 설명함)

  • 변수 선언 키워드(let, const, var)와 스코프와의 관계를 설명할 수 있다.

    o
    변수를 선언하기 위한 키워드에는 3가지가 있다.
    ES6 이전에는 var만 있었는데 그 이후로는 let과 const가 생겼다.
    var는 화살표 함수, function scope를 제외한 block scope를 무시하여 값을 내기도 하고, 값을 재할당하거나 재선언하는 것에 자유롭기 때문에 혼란을 야기한다.
    따라서 이제 사용하는 것을 권장하지 않는다.

    let은 함수, 블록 스코프의 범위를 따르며, 값을 재할당할 수 있으나 재선언은 불가하다.
    const는 함수, 블록 스코프의 범위를 따르며, 값을 재할당하거나 재선언 하는 것이 불가하다.

  • 전역 객체가 무엇인지 설명할 수 있다.

    o
    대표적 window 객체
    전역 객체는 어디서든 접근이 가능하다.
    어디서든 접근이 가능하기 때문에 의도와 다른 값이 할당되거나
    다른 로직이 포함되기도 하는 등의 side effect이 발생한다.
    side effect를 줄이기 위해서는 전역 변수를 최소화하는 것이 좋다.

  • 클로저 함수의 정의와 특징에 대해서 이해할 수 있다.

    o
    A closure is the combination of a function bundled together (enclosed) with
    references to its surrounding state the lexical environment.

    = A closure is the combination of a function and the lexical environment
    within which that function was declared.
    This environment consists of any local variables
    that were in-scope at the time the closure was created.

    즉, 클로저 = 함수 + 함수가 접근할 수 있는 변수 (함수가 함수를 리턴하는 경우가 많기 때문)
    원래라면, 함수 안에서 선언된 변수와 매개변수는
    그 함수 내에서만 사용이 가능하며 함수 밖에서 접근할 수 없다.
    그러나 클로저를 사용하면, 근접한 함수 내의 변수를 가져올 수도 있고, 접근할 수도 있다.

  • 클로저가 갖는 스코프 범위를 이해할 수 있다.

    o
    일단 둘의 차이에 대해 살펴보자.
    클로저는 클로저의 함수가 어디에서 호출되는지는 중요하지 않다.
    그저 선언된 함수 주변 환경에 따라 접근할 수 있는 변수가 무엇인지가 중요하다.

    반면, 스코프는 변수에 접근 가능한 범위 내에서만 함수 호출이 되므로
    함수가 어디에서 호출되는지가 중요하다.

    클로저가 갖는 스코프의 범위는 다음과 같다.
    두 개의 함수가 있다고 하자. 외부에 있는 함수는 외부함수(outFnc()),
    내부에 있는 함수는 내부함수(inFnc)라고 한다.
    외부함수는 전역스코프에 있는 변수(imGlobalVar)를 가져와 사용할 수 있다.
    내부함수는 전역스코프에 있는 변수와 그 위에 있는 외부함수의 변수를 모두 가져와 사용할 수 있다.
    이렇게 클로저는
    내부(inner) 함수가 외부(outer) 함수의 지역 변수에 접근할 수 있다.

    ```
    const imGlobalVar = 'global variable'   //전역스코프에 있는 전역 변수
    
    	function outFnc(){      //외부함수
    		const outFncVar = 'outer function';   
    		const inFnc = function(){    //내부함수
    			return '나는' + outFncVar + '랑' + imGlobalVar + '에 모두 접	근 가능하지!';
    	}
    		return inFnc;
    }
    
    const inFncOnGlobal = outFnc();
    const message = inFncOnGlobal();
    console.log(message);
    	
    ```
  • 클로저를 이용해 유용하게 쓰이는 몇 가지 패턴을 이해할 수 있다.

    클로저를 사용하는 대표적인 경우가 3가지가 있다. (the use case of the closure)
    리턴한 새로운 함수의 클로저에 데이터가 보존되도록 하여 데이터를 보존하는 함수와
    여러 전달인자를 가진 함수를, 함수를 연속적으로 리턴하는 함수로 변경하는 커링의 경우
    데이터를 다른 코드 실행으로부터 보호하는 정보 은닉(information hiding)인 캡슐화를(encapsulation)위한 독립적인 기능을 갖춘 모듈패턴을 구현하기 위해 사용함.

    함수 팩토리(function factory)란, 새로운 함수를 동적으로 생성하여 반환하는 함수를 말한다.
    이 패턴을 사용하면 코드를 더 모듈화하고 재사용 가능한 함수를 만들 수 있다.

    이 패턴을 사용할 때, 팩토리 함수 내부에서 생성되는 함수의 변수와 함수들은 해당 함수의 지역 범위에 속한다.
    따라서, 이 변수와 함수들은 외부에서 접근이 불가능하다.
    이러한 기능을 이용하여, 팩토리 함수를 사용하는 코드에서
    해당 변수와 함수들을 은닉하고 보호할 수 있습니다.
    이러한 변수와 함수들을
    "프라이빗 변수(private variables)"와 "프라이빗 함수(private function)"라고 부르며, 외부에서 직접 접근이 불가능하기 때문에 캡슐화(encapsulation)의 개념과 유사하다.

    이러한 기능을 활용하여, 팩토리 함수에서 반환되는 함수들은 내부적으로 프라이빗 변수와 함수들을 참조하여 동작한다.
    이를 통해, 반환되는 함수들은 팩토리 함수에서 정의한 규칙에 따라 동작하며,
    이를 "네임스페이싱(namespacing)"이라고 부른다.
    이러한 네임스페이싱은 다른 함수나 객체와 이름 충돌을 피할 수 있게 해주며,
    코드의 가독성과 유지보수성을 높일 수 있게 해준다.


+) 2023.05.03 복습 후, 추가하는 내용
1)

function inner () {
   let x
   x = x + 20;
   return x;
 }
 inner();

// NaN을 반환한다.

inner()함수에 변수는 x다. x는 선언만 되어있고, 값이 할당되어 있지 않으므로undefined로 처리되었다.
따라서 undefined에 숫자를 더하면 NaN이 된다.
이를 해결하기 위해서는 값을 할당하기 전에 x를 초기화해야 한다.
예를 들어 첫 줄을 let x = 0;으로 변경하여 x를 0으로 초기화할 수 있다.


2)

let x = 10;

function outer () {
  x = 20;
  function inner () {
    x = x + 20;
  }
  inner();
}

outer();
let result = x;

//result의 값은 40이다.

지역변수는 전역변수보다 우선순위에 있기 때문에 지역스코프 내에서 선언된 변수가 우선이 된다.
그러나 여기서는 전역 변수를 제외하고는 제일 안쪽 지역스코프 inner()함수나, 그 바깥 지역스코프 outer()함수에서 별도로 선언된 변수를 발견할 수 없다.
따라서 변수는 전역 변수에서만 선언되었고, 이후에는 재할당 된 것이다.
따라서 제일 마지막으로 값이 할당된 inner()함수에서의 x = x+20으로 전역변수의 값이 할당된다.
따라서 클로저에 의해 outer()함수에서 받은 x = 20을 가져와
inner()함수에 넣고 나온 값인 40이 재할당된다.
따라서 전역 변수 10은 2번에 걸쳐 재할당 된 후, 마지막으로 할당된 값으로 값을 반환한다.
(10 --> 20 --> 40이므로 x를 호출하면, 최종적으로 40을 반환)
따라서 변수 x의 값을 result 변수에 할당했으므로 result의 값은 40이다.


3)
JavaScript에서 함수는 객체(Object)다.
객체는 참조자료형(Reference Type)으로, 변수에 객체를 할당하면 변수에는 해당 객체의 참조값이 저장된다.

예를 들어, 객체를 생성하고 다른 변수에 할당한 뒤, 그 변수를 통해 객체를 수정하면 원래의 객체도 함께 변경된다.
하지만 함수를 생성하고 다른 변수에 할당한 경우, 그 변수를 통해 함수를 수정하더라도 원래의 함수는 변경되지 않는다.

따라서 함수를 다른 변수에 할당하면, 그 변수에는 해당 함수의 참조값이 저장된다.
그리고 함수를 호출할 때마다
함수의 실행 컨텍스트(Execution Context, 코드가 실행되기 위해 필요한 정보를 모아놓은 객체)가 생성되며,
이 실행 컨텍스트에는 함수의 지역 변수와 매개변수가 저장된다.
이때 매개변수와 지역 변수는 해당 실행 컨텍스트에만 존재하며,
함수의 실행이 끝나면, 실행 컨텍스트가 파괴되며 해당 변수들도 함께 사라진다.

따라서 함수를 똑같이 이용했지만,
완전히 새로운 객체의 주솟값이 각각 담기기 때문에 서로 다른 주솟값을 가진다.
즉, 같은 함수를 호출하더라도 각 호출마다 실행 컨텍스트가 생성되며,
해당 컨텍스트에는 매개변수와 지역 변수가 새롭게 생성되고, 이로 인해 같은 함수라 하더라도
각 호출 시 마다 다른 주소값을 가지게 된다.


소감

🔡➡️💻➡️🤓👍

오늘은 자료형에서 시간을 많이 할애했다.
원시형과 참조형에서 이해해다고 생각했는데
얕은 복사와 깊은 복사 부분에서 앞의 내용이 뒤죽박죽 섞여서
너무나 헷갈렸다.
모르는 건 그냥 못 지나치고, 짚고 넘어가야 하는 성격이라
시간이 매우 많이 걸린다.
그렇지만, 나의 언어로 한번 이해하면,
그 지식은 쉽게 사라지지 않는다.
나의 노력이 담긴 것이니 얼마나 의미있는가.
이번 한 주 수업도 하루 남았다.
시간이 모자라다는 느낌이 많이 드는 요즘이다.
그만큼 시간을 잘 쓰고 있다는 의미겠지.
토요일에는 푹 쉴 예정이다.
내일은 저녁에 양꼬치 먹고 하루 푹 쉬어야지~ 😝

profile
developerpyk

0개의 댓글