TIL 2020-10-21 (Scope)

nyongho·2020년 10월 20일
0

JavaScript

목록 보기
5/23

Level 2-2 Scope


TIL List

  • Scope

1) Scope 의 정의

Scope를 우리말로 번역하면 ‘범위’라는 뜻을 가지고 있다. 즉, 스코프(Scope)란 ‘변수에 접근할 수 있는 범위’라고 할 수 있다.

"그 변수가 이 코드 중 어디까지 영향을 미칠 수 있는가?" 라고도 설명할 수 있겠다.

우선 아래 코드를 봐보자.

let greeting = 'Hello';
function greetSomeone() {
  let firstName = 'Yongho';
  return greeting + ' ' + firstName;
}

greetSomeone(); // => ???
firstName; // => ???

출력될 값을 충분히 고민해 본 후 아래 코드를 봐보길 바란다.

let greeting = 'Hello';
function greetSomeone() {
  let firstName = 'Yongho';
  return greeting + ' ' + firstName;
}

greetSomeone(); // => 'Hello Yongho'
firstName; // => ReferenceError (firstName is not defined)

greetSomeone() 함수를 실행하면 위 코드를 보면 알 수 있듯이 'Hello' + ' ' + 'Yongho'
즉, 'Hello Yongho' 를 출력하게 된다.

근데 여기서 firstName 을 쳐보면 에러 메시지로 (firstName 은 변수로 선언 되지 않았습니다) 라고 뜬다.

"아니 우리는 let firstName = 'Yongho' 라고 분명 선언을 해줬는데 뭐지?!"

여기서 ‘변수에 접근할 수 있는 범위’ 즉, Scope(범위)를 배워야 하는 이유가 생긴다.


2) Scope에는 두 가지 타입이 있다.

1) Global Scope (전역 스코프)

Global Scope (전역 스코프)는 말 그대로 모든 곳에서 해당 변수에 접근 할 수 있다는 의미이다.

let greeting = 'Hello';
function greetSomeone() {
  let firstName = 'Yongho';
  return greeting + ' ' + firstName;
}

greetSomeone(); // => 'Hello Yongho'
firstName; // => ReferenceError (firstName is not defined)

위 코드에서 전역 스코프에 해당하는 코드는 무엇일까?

답은 아래와 같다.

let greeting = 'Hello';

greetSomeone(); // => 'Hello Yongho'
firstName; // => ReferenceError (firstName is not defined)

특정 지역에 연루되지 않고 바깥에 있는 애들은 전역 스코프에 속해 있다고 생각하면 된다.

따라서 전역 스코프에 선언된 변수는 어느곳에서든지 그 값을 참조할 수 있다.

아직까지 잘 이해가 안돼도 우선 아래를 봐보자.

2) Local Scope (지역 스코프)

Local Scope (지역 스코프)는 해당 지역에서만 접근할 수 있기 때문에 범위를 벗어난 다른 지역에서는 접근 할 수 없다는 의미이다.

let greeting = 'Hello';
function greetSomeone() {
  let firstName = 'Yongho';
  return greeting + ' ' + firstName;
}

greetSomeone(); // => 'Hello Yongho'
firstName; // => ReferenceError (firstName is not defined)

위 코드에서 지역 스코프에 해당하는 코드는 무엇일까?

답은 아래와 같다.

function greetSomeone() {
  let firstName = 'Yongho';
  return greeting + ' ' + firstName;
}

함수 greetSomeone() 을 따로 선언함으로써 얘네들은 자신들만의 지역 스코프를 만들게 됐다.

따라서 지역 스코프에서 선언된 변수는 바깥의 지역에 영향을 주지 못한다.

이해하기 쉽게 법률로 따지자면 Global Scope (전역 스코프)에서 선언된 변수는 국가에서 지정한 법이다.

따라서 누구든지 법의 영향을 받는다.

Local Scope (지역 스코프)에서 선언된 변수는 우리 가족끼리 정한 법이다.

우리 가족에게는 영향을 줄 수 있지만, 그 외 다른 사람들에게는 영향을 주지 못한다.

이렇게 설명하면 이해하기 쉬울 것이라고 생각한다.

이러한 Scope의 특징을 정리하면 아래와 같다.


3) Scope 의 특징

  • 변수는 특정 환경 내에서만 사용 가능하며, 프로그래밍 언어는 각각의 변수 접근 규칙을 가지고 있다.

  • Scope는 변수와 그 값이, 정확히 어디서부터 어디까지 유효한지를 판단하는 범위다.

  • JavaScript 에서는 기본적으로, 함수가 선언되는(lexical) 동시에 자신만의 Scope(범위)를 가지게 된다.

  • Scope 는 중첩이 가능하다.
    (함수 안에 함수를 넣을 수 있다.)

  • 전역 변수는 어디서든 접근이 가능하다.

  • 지역 변수는 해당 함수 내에서 전역 변수보다 더 높은 우선순위를 가진다.


4) 문제로 알아보는 Scope의 특징들

문제 1 (Global Scope 와 Local Scope의 적용 범위)

let name = "Richard";

function showName() {
  let name = "Jack"; // 지역 변수
  // showName 함수 안에서만 접근 가능
  console.log(name);
}

console.log(name); // ???
showName(); // ???
console.log(name); // ???

위와 같이 함수를 선언했을 때 각각 어떤 값을 출력할까? A 부터 Z 까지 천천히 해보자.

우선 let name = "Richard"; 은 전역 스코프에 선언된 변수다. 즉, 전역 변수다.

그리고 let name = "Jack"; 은 지역 스코프에 선언된 변수다. 즉, 지역 변수다.

따라서 처음 console.log(name) 을 입력하면 당연히 전역 변수인 "Richard" 를 참조한다.

그 다음, showName(); 을 입력하면 Scope 의 특징 중 마지막 특징에 따라서 지역 변수인 "Jack" 을 참조하게 된다.

마지막으로 console.log(name); 을 입력하면 변함없이 전역 변수인 "Richard" 를 참조해서 출력한다.

어렵지 않지 않은가? 그럼 다음 문제를 봐보자.

문제 2 (Global Scope 와 Local Scope의 적용 범위)

let name = "Richard";

function showName() {
  name = "Jack"; // 전역 변수
  // 선언(let)이 없기 때문에, 바깥 scope에 있는 name이라는 변수를 가져옵니다
  console.log(name);
}

console.log(name); // ???
showName(); // ???
console.log(name); // ???

그 전 문제와 다른 점은, 지역 변수 name 이 let 으로 선언되지 않았다는 점이다.

이럴 경우 아까와 다른 출력을 갖게 될까? 한 번 고민해보고 스크롤을 내리기 바란다.

자, 우선 처음 console.log(name); 을 입력하면 당연히 전역 변수인 "Richard" 를 참조해 출력한다.

그 다음, showName(); 을 입력하면 name 은 지금 따로 선언이 안됐기 때문에 전역 변수인

let name = "Richard" 를 참조하게 되어 전역 변수의 값에 "Jack" 이 할당된다. 따라서 showName(); 을 입력하면 "Jack" 을 출력한다.

갑자기 뭔 뚱딴지 같은 소리인지 머리가 복잡해질 수도 있다. 괜찮다. 나도 그랬다.

먼저 아래 코드를 봐보고 이해를 해보자.

let name = "Richard";
console.log(name) // "Richard"
name = "Jack";
console.log(name) // "Jack"

우리는 그 전 개념에서 기존에 선언된 변수에 새로운 값을 할당하는 방법을 배웠다.

위 코드도 마찬가지다. 지역 스코프 내의 name 은 따로 선언이 안됐으므로 기존에 선언된 변수에 새로운 값을 할당하게 되는 것이다. 따라서 이미 선언된 전역 변수인 let name = "Richard" 의 값에 "Jack" 을 할당하게 되므로 showName(); 은 "Jack" 이 되는 것이다.

마지막, console.log(name); 을 입력하면 전역 변수는 let name = "Jack" 으로 새로운 값을 할당 받았으므로 "Jack" 이 된다.

따라서 결과는 아래와 같다.

console.log(name); // Richard
showName(); // Jack
console.log(name); // Jack

자, 그럼 여기서 조건 하나만 더 붙혀보자. 만약 매개변수 값으로 name 을 넣어주면 어떻게 될까?

let name = "Richard";

function showName(name) {
  name = "Jack"; 
  console.log(name); 
}

console.log(name); // Richard
showName(); // Jack
console.log(name); // Richard

아니 위에서 분명 선언되지 않은 변수는 전역 변수를 참조한다고 했는데 3번째에서 다시 Richard 가 출력되는 모습이다. 뭐가 문제인걸까?

우리가 아직 알고 있지 못한 사실이 하나 있다.

바로 함수의 매개변수 또한 함수 내에서 정의되는 지역 변수처럼 동작한다는 점이다.

지역 변수는 함수 내에서 전역 변수보다 더 높은 우선순위를 가진다는 점을 위에서 언급했다.

따라서 매개변수 name 이 함수 내에서 가장 높은 우선순위를 가진것이며 이 선언된 변수에

name = "Jack"; 즉, 바로 상단의 스코프인 매개변수 name에 값을 할당한 것이다.

그래서 전역 변수인 let name = "Richard" 에는 범위가 못 미쳐 영향을 못주는 것이다.

(이거 혼자 이해하는데 엄청 오래 걸렸다.. ㅠㅠ)

최대한 내가 이해한 방식을 풀어서 쓰려고 노력하고 있다. 만약 이해가 안되는 내용이 있으면 댓글을 남겨주길 바란다.

문제 3 (Block 의 개념 및 활용)

다음 문제를 보기전에 아래 개념을 확실히 하고 가자.

Block은 { } 와 같이 중괄호로 시작하고, 끝나는 단위를 뜻한다.
Block 범위 안에 선언된 변수는 Block 범위를 벗어나면 사용할 수 없다.

for(let i=0; i<5; i++) {
  console.log(i); // 다섯번 iteration
}
console.log('final i:', i); // ?

결과는 어떻게 출력되겠는가? 한 번 진지하게 고민해보고 스크롤을 내리기 바란다.

위 문제는 "Block {} 안의 변수 i 가 Block 바깥에서도 영향을 미치는가?" 를 물어보는 문제다.

위에서 언급한 개념과 같이 Block 범위를 벗어나는 즉시 변수를 사용할 수 없으므로 영향을 줄 수 없으므로 결과는 ReferenceError (i is not defined) 를 나타나게 된다.

문제 4 (3가지 선언 방법의 차이점)

이번에는 변수의 3가지 종류 중 한 가지인 var 를 사용해 코드를 재작성 해보겠다.

for(var i=0; i<5; i++) {
  console.log(i); // 다섯번 반복
}
console.log('final i:', i); // ?

위 경우에는 어떤 결과를 출력하게 될까? (var 개념도 모르는데 그걸 어떻게 알아요 아저씨;)

우선 답부터 말하자면 0,1,2,3,4를 출력하고 마지막으로 'final i: 5' 를 출력하게 된다.

그렇다면 우리는 위의 사실을 통해 let 과 var 은 선언한다는 공통점을 같지만 변수가 적용되는 범위가 다르다는 것을 알 수 있다.

먼저 let 과 var 둘의 차이점을 간략하게 적겠다.

  1. let 은 Block Scope, var 은 Function Scope 이다.
  2. 따라서 var로 선언했을 경우 Block 범위를 벗어나도 같은 Function Scope 내에서는 사용이 가능하다.

이를 간단하게 설명하자면 var 로 선언된 변수는 적용되는 범위가 let 보다 넓고 let은 무조건

Block 범위 내에서만 적용되지만 var 은 Block 범위 바깥에서도 적용된다는 뜻이다.

(여담으로 var 선언 방법은 내가 알기로는 구식 기술(old way) 이라고 알고 있다.)

따라서 앞으로 코딩 할 때는 var 보다는 let 을 선언하는게 훨씬 좋다. 그 이유는 아래 코드를 보면 쉽게 이해가 될 것이다.

let myName = 'Yongho'; // myName = 'Yongho'
let myName = 'Wonwoo'; // Error, 이미 myName 변수가 선언 됐으므로 에러가 뜬다.

var myName = 'Yongho'; // myName = 'Yongho'
var myName = 'Wonwoo'; // myName = 'Yongho' ➡ myName = 'Wonwoo'

두 선언의 차이가 느껴지는가?

let 은 재선언이 안되지만 var은 재선언이 가능한 모습이다.

실제로 코딩을 할 때 재선언을 하는 경우는 대부분 '버그' 인 상황이며 let 키워드가 이러한 실수를 막아준다.

마지막으로 const 라는 선언 방법이 있다.

  1. const 는 값이 변하지 않는 상수, 즉 상수를 정의할 때 사용하는 키워드다.
  2. let 키워드와 동일하게 Block Scope 를 따른다.
  3. 값을 재할당 하려고 하면 TypeError 를 낸다.

우선 아래 코드를 봐보자.

let a = 1;
a = 2;
console.log(a) // 2

let 은 위와 같이 값을 재할당 할 수 있었다. 하지만 const 는 한 번 값을 할당하면 재선언이 불가능하다.

const a = 1;
a = 2;
console.log(a) // TypeError: Assignment to constant variable.

저 에러코드를 해석하자면 상수 변수에 이미 할당이 됐다는 뜻으로 값을 재할당 할 수 없다는 뜻이다.


5) 전역 변수와 window 객체

전역 범위를 대표하는 객체를 window 라고 한다.

따라서 전역 스코프에서 선언된 함수, 그리고 var 키워드를 이용해 선언된 변수는 window 객체와 연결된다.

var myName = 'Yongho'
console.log(window.myName) // 'Yongho'

var 로 선언된 변수는 window 객체와 연결되는 모습.

function test () {
 console.log('idk');
}

console.log(test === window.test); // true

전역 스코프에서 선언된 함수는 window 객체와 연결되는 모습.


6) 주의해야 할 Scope 사항

1. 절대로! 선언 키워드 (let, var, const) 없이 변수를 초기화 하지 말 것.

(여기서 초기화라는 뜻은 값을 처음 할당 한다는 뜻이다.)

function showAge () {
age = 90; // 선언을 안하면 전역 변수로 취급된다.
console.log(age);
}

showAge(); // 90
console.log(age); // 90

위 코드를 보면 알 수 있듯이 age 는 선언이 안된 상태다. 이러할 경우에 컴퓨터는 age를 자동으로 전역 변수로 취급해버린다. 따라서 아래와 같은 상황이 되버리는 것이다.

let age = 90;
function showAge () {
console.log(age);
}

showAge(); // 90
console.log(age); // 90

2. 이를 방지 하기 위한 메소드 ('Strict Mode')

코드의 맨 앞 부분에 'use strict' 라는 키워드를 사용하면 코드 내 문법적으로 실수할 수 있는 부분들을 에러로 나타내줘 실수를 미연에 방지할 수 있다.

아래와 같이 사용하면 된다.

'use strict'
function showAge () {
age = 90; // 선언을 안하면 전역 변수로 취급된다.
console.log(age);
}

showAge(); // ReferenceError: age is not defined (age 는 선언된 적이 없다.)
console.log(age); // 
profile
두 줄 소개

0개의 댓글