게시글

5만명이 선택한 평균 별점 4.9의 제로초 프로그래밍 강좌! 로드맵만 따라오면 됩니다! 클릭
강좌20 - JavaScript - 8년 전 등록 / 5년 전 수정

함수의 범위(scope)

lexical scoping

안녕하세요. 이번 시간에는 함수 스코프(scope)에 대해서 설명드리겠습니다. 스코프는 범위라는 뜻입니다.

전역 변수와 지역 변수

자바스크립트에서 주로 변수를 사용해 데이터를 저장했었는데요. var로 선언했었죠? 물론 변수를 만드는 일은 문제가 되지 않지만, 전역(global)변수를 만드는 일은 최대한 지양해야합니다. 전역변수란 자바스크립트에서 제일 바깥 범위(함수 안에 포함되지 않은)에 변수를 만드는 건데요. window 객체에 변수를 만드는 겁니다.

var x = 'global';
function ex() {
  var x = 'local';
  x = 'change';
}
ex(); // x를 바꿔본다.
alert(x); // 여전히 'global'

다음을 보시면 같은 x여도 ex 함수 바깥의 x는 전역변수고, ex 함수 안의 x는 ex 함수의 지역변수입니다. 지역 변수는 함수 안에 들어있는 변수를 의미합니다. var x = 'global'을 한 게 왜 전역변수이고, window 객체의 속성이 되는지 모르겠다면, window 강좌 를 다시 복습하세요!

스코프(Scope)

위의 상황에서 지역변수는 아무리 해도 전역변수에 영향을 끼칠 수 없습니다. 바로 함수 스코프 때문이죠. 범위라는 말처럼 함수 안에서 선언된 변수는 해당 함수 안에서만 사용할 수 있습니다. var x = 'local'은 ex 함수 안에서만 그 데이터를 사용할 수 있습니다. 그 밑에 x = 'change'도 함수 안의 지역변수 x를 바꾸는 겁니다. 밑에 아까와는 살짝 다른 예를 보시죠.

var x = 'global';
function ex() {
  x = 'change';
}
ex();
alert(x); // 'change'

아까와는 달리 ex 함수에 안에서 var을 선언하지 않았습니다. 이제는 x = 'change'를 했을 때 전역변수가 바뀌는데요. 자바스크립트는 변수의 범위를 호출한 함수의 지역 스코프부터 전역 변수들이 있는 전역 스코프까지 점차 넓혀가며 찾기 때문입니다. 함수 ex의 범위 안에 x가 없기 때문에 더 넓은 범위인 전역 스코프에서 찾는거죠. 정확한 원리는 다음 시간 실행 컨텍스트에서 배울 수 있습니다. 첫 번째 예에서는 ex의 범위 안에 바로 x(ex의 지역변수)가 있었기 때문에 지역변수 x를 바꾸고, 전역변수 x는 바꾸지 않았습니다.

스코프 체인

바로 전역변수와 지역변수의 관계에서 스코프 체인(scope chain)이란 개념이 나옵니다. 내부 함수에서는 외부 함수의 변수에 접근 가능하지만 외부 함수에서는 내부 함수의 변수에 접근할 수 없습니다. (아래 enemy가 undefined인 것을 보시죠) 그리고 모든 함수들은 전역 객체에 접근할 수 있죠. 위의 예제와 비슷한 예제를 하나 더 보여드리겠습니다.

var name = 'zero';
function outer() {
  console.log('외부', name);
  function inner() {
    var enemy = 'nero';
    console.log('내부', name);
  }
  inner();
}
outer();
console.log(enemy); // undefined

inner 함수는 name 변수를 찾기 위해 먼저 자기 자신의 스코프에서 찾고, 없으면 한 단계 올라가 outer 스코프에서 찾고, 없으면 다시 올라가 결국 전역 스코프에서 찾습니다. 다행히 전역 스코프에서 name 변수를 찾아서 'zero'라는 값을 얻었습니다. 만약 전역 스코프에도 없다면 변수를 찾지 못하였다는 에러가 발생합니다. 이렇게 꼬리를 물고 계속 범위를 넓히면서 찾는 관계를 스코프 체인이라고 부릅니다.

렉시컬 스코핑(lexical scoping)

많이들 헷갈리는 개념인데 스코프는 함수를 호출할 때가 아니라 선언할 때 생깁니다. 호출이 아니라 선언요! 정적 스코프라고도 불립니다. 다음 코드에서 console이 어떻게 찍힐 지 예상해보세요.

var name = 'zero';
function log() {
  console.log(name);
}

function wrapper() {
  name = 'nero';
  log();
}
wrapper();

쉽죠? nero입니다. log를 호출하기 전에 name을 'nero'로 바꿨거든요. 그럼 다음은요? 문제를 살짝 바꿨습니다.

var name = 'zero';
function log() {
  console.log(name);
}

function wrapper() {
  var name = 'nero';
  log();
}
wrapper();

똑같이 nero 아니냐고요? 땡! zero입니다. 스코프는 함수를 선언할 때 생긴다고 했죠? log 안의 name은 wrapper 안의 지역변수 name이 아니라, 전역변수 name을 가리키고 있는 겁니다. 이런 것을 lexical scoping이라고 합니다. 한글로는 어떻게 번역해야 할지 모르겠네요. 직역하면 어휘적 범위인데 좀 어색하죠? 정적 스코프가 더 나아 보이네요.

lexical scoping이 좀 이해하기 힘들기 때문에 다시 설명드리겠습니다. 함수를 처음 선언하는 순간, 함수 내부의 변수는 자기 스코프로부터 가장 가까운 곳(상위 범위에서)에 있는 변수를 계속 참조하게 됩니다. 위의 예시에서는 log 함수 안의 name 변수는 선언 시 가장 가까운 전역변수 name을 참조하게 됩니다. 그래서 wrapper 안에서 log를 호출해도 지역변수 name='nero'를 참조하는 게 아니라 그대로 전역변수 name의 값인 zero가 나오는 겁니다.

무슨 짓을 해도 log 함수가 한 번 선언된 이상, 전역변수를 가리키게 되어있는 name 변수가 다른 걸 가리키게 할 수 없습니다. 유일한 방법은 아까처럼 전역변수를 다른 값으로 바꾸는 겁니다. 다음 시간에 실행 컨텍스트에 대해서 배울텐데 그 때 정확한 원리를 설명드리겠습니다.

전역변수를 만드는 일은 지양하라고 했는데, 그 이유는 변수가 섞일 수 있기 때문입니다. 자바스크립트 앱을 만들면서 혼자만 개발하는 게 아니라, 여러 명과 협동도 하고, 다른 사람의 라이브러리(자바스크립트 코드 모음)를 사용하는 일도 많습니다. 그런데 전역변수를 사용하다보면, 우연의 일치로 인해 같은 변수 이름을 사용해서 이전에 있던 변수를 덮어쓰는 불상사가 발생할 수 있습니다. (특히 $나 _같은 유니크한 변수들은 경쟁이 치열합니다)

간단한 해결 방법은 전역 변수 대신 한 번 함수 안에 넣어 지역변수로 만드는 겁니다. 아니면 객체 안의 속성으로 만들 수도 있습니다.

var obj = {
  x: 'local',
  y: function() {
    alert(this.x);
  }
}

위 처럼 하면 obj.x, obj.y() 이렇게 접근해야 하기 때문에 다른 사람과 섞일 염려가 없죠. obj를 통째로 덮어쓰지 않는 이상은요. 전역변수를 하나로 최소화해서 변수가 겹칠 우려도 최소화하는 거죠.

이런 방법을 네임스페이스를 만든다고 표현합니다. obj라는 고유 네임스페이스를 만들어서 겹치지 않게 하는 거죠. 대부분의 라이브러리가 네임스페이스를 사용하고 있죠. naver는 jindo, facebook은 FB, jquery는 jQuery(또는 $)같이요.

하지만, 위 방법의 단점은 누군가 고의적으로 x와 y를 바꿀 수 있습니다. 코드 밑에 스크립트를 추가해서요. obj를 통째로 바꾸지 않더라도 밑에 obj.x = 'hacked'; 라고 한 줄 추가만 하면 obj.y();를 했을 시 local 대신 hacked가 alert됩니다. 그것을 방지하려면

var another = function () {
  var x = 'local';
  function y() {
    alert(x);
  }
  return { y: y };
}
var newScope = another();

조금 복잡하지만 위와 같이 하면 됩니다. another(); 하는 순간 return에 의해 { y: function () { alert(x) } }; 가 newScope에 저장됩니다. 이제 newScope라는 네임스페이스를 통해서 y에 접근할 수 있습니다. x는 접근할 수 없죠. 위처럼 함수로 감싼 후 return을 통해 공개할 변수(y)만 공개하고 비공개할 변수(x)는 비공개하는 방법을 취할 수 있습니다. 즉, return하는 변수는 공개 변수고, 다른 것은 비공개 변수인거죠.

위의 코드를 간략하게 바꾸면

var newScope = (function () {
  var x = 'local';
  return {
    y: function() {
      alert(x);
    }
  };
})();

위와 같이 쓸 수 있습니다. another같은 변수를 한 번 거치는 대신, newScope에 바로 집어넣은거죠. 처음보는 게 나왔는데요. (function() {})(); 구문입니다. IIFE(즉시 호출 함수 표현식)이라고도 하고, 모듈 패턴이라고도 하는데, 함수를 선언하자마자 바로 실행시켜버리는 거죠. 함수를 function() {}로 선언하면서 동시에 ()를 붙이니까 즉시 실행됩니다. 이 구문이 라이브러리를 만들 때 기본입니다. 많은 라이브러리가 이 구문을 활용하고 있습니다. 비공개 변수가 없는 자바스크립트에 비공개 변수 기능을 만들어주기 때문이죠. 이 패턴은 꼭 기억하고 있어야합니다!

위의 패턴의 원리를 알려면 실행 컨텍스트에 대해 알아야 합니다. 다음 시간에는 제일 중요한 개념인 실행 컨텍스트와 그것을 활용한 클로저에 대해 알아보겠습니다.

조회수:
0
목록
투표로 게시글에 관해 피드백을 해주시면 게시글 수정 시 반영됩니다. 오류가 있다면 어떤 부분에 오류가 있는지도 알려주세요! 잘못된 정보가 퍼져나가지 않도록 도와주세요.
Copyright 2016- . 무단 전재 및 재배포 금지. 출처 표기 시 인용 가능.
5만명이 선택한 평균 별점 4.9의 제로초 프로그래밍 강좌! 로드맵만 따라오면 됩니다! 클릭

댓글

15개의 댓글이 있습니다.
4년 전
1. function안에 있는 변수는 function 바깥으로 빠져나가지 못한다.
2. 변수 값을 판단하는 기준은 scope chain에 따라서 위로 물고 물면서 올라가서 값을 지정한다.
3. 렉시컬 스코프에 의해서 변수든 함수든 선언된 순간 스코프가 정해진다.
4. 비동기 함수(setTimeout 함수)안에 있는 function() 안에 있는 변수의 값은 "실행" 될때 결정된다.

여기서 4번에서 비동기 함수안에서의 콜백함수에서의 변수값은 실행될때 결정되므로 렉시컬 스코프가 적용이 되지 않는 예외적인 상황이 맞나요?
4년 전
4번이 무슨 의미이신지 모르겠습니다. 그대로 렉시컬 스코프를 따릅니다. 실행될 때 결정되는 것은 보통 this가 있습니다.
4년 전
for (var i = 0 ; i \u003c 100; i++) {
setTimeout(function() {
console.log(i)
}, j * 1000);
}
강의에서 이 코드를 예를 드셨는데 setTimeout(function(){console.log(j)}, j*1000)에서의 콜백함수인 function(){console.log(i)}도 렉시컬 스코프를 따른다는건가요?
4년 전
제가 이해를 잘못하고 있었네요
for(var i=0; i\u003c100; i++){
setTimeout(function(){ console.log(i) }.i*1000)
} 에서
비동기함수인 setTimeout의 파라미터로 있는 익명함수의 변수값은 setTimeout이 실행되고나서 결정된다는거네요. 렉시컬 스코프에 의해서 익명함수가 선언된 순간 익명함수에 있는 변수 i의 scope는 for(var i=0; i\u003c100; i++)가 있는 global scope가 되는거죠 ?
4년 전
https://medium.com/@ItsMeDannyZ/react-hooks-slider-how-to-build-an-image-slider-with-autoplay-part-2-c94deaf763c4 \u003c- 여기서 리액트로 슬라이더를 만들고있던 도중에 막히는게 있어서 질문 남깁니다. useEffect On Initial Slider Render 라는 제목의 내용에서 막히게 되었는데 클로저 개념에 대해서 파악하면 이해를 할 수 있을지 궁금합니다.
4년 전
클로저 개념은 js라면 어디든 나옵니다. 저 부분에 대해서만 적용되지는 않습니다.
5년 전
안녕하세요! 궁금한게있어서 질문드립니다.
위의 lexical scoping에서 예시를 들은것중에 질문이있습니다.
전역변수선언을 조심하라는 예씨로 nero , zero 리턴값을 각각보여주시고 함수는 상위함수의 스코핑의 변수를 받아들인다고하셨는데요.
저는
```js
var name = 'zero';
function log() {
console.log(name);
}

function wrapper() {
var name = 'nero';
log();
}
wrapper();
```
이 예제에서 window객체에 zero라는 변수가 할당되었고
그 안에 log(), wrapper 라는 메소드들이 각각 선언되어서 각각의 스코프범위를 가지고 있다고 생각하고 log의 메소드랑 wrapper는 겹치는 부분이 없기에 상위범위를 window라고 인식해서 그 범위에 있는 zero변수값을 가져다쓴것 이라고 이해를 했습니다.

제가 만든 코드입니다.
```js
var name ='zero';
function wrapper() {
name ='nero';
log();
function log() {
console.log(name);
}};

wrapper();
```
이런식으로 처음의 예제와 달리 log함수의 물리적(시각적으로 보이기에)인 상위함수를 wrapper로 만들어서 alert을 nero로만들려고 했고 리턴도 nero로 나왔습니다.
스코프의 범위를 이렇게 물리적(시각적)으로 보면되는걸까요?
5년 전
넵 따로 머리로 계산해야 하는 부분이 없습니다. 진짜 상위 함수만 타고 올라가면서 변수를 찾으면 됩니다.
5년 전
와.. 찾아본 글들 중에 가장 확실하게 머리에 쏙쏙 박혔습니다. 정말 감사합니다..
5년 전
lexcial scoping에 관해선데요. 가장 가까운 범위에 있는 변수를 참조한다는 표현보다는 "선언 시에 알 수 있었던" 이라는 표현은 어떨까요
5년 전
"선언 시에 알 수 있었던"이라고 표현하면 "어떻게 알 수 있는가?"라는 부가적인 질문이 생길 것 같습니다.
6년 전
경력이 적지 않은 자바개발자입니다. front-end가 더 잘 맞아 전향해서 생활코딩과 제로초님 튜토리얼 보면서 공부중인데요. 당연한 코드가 당연한 코드가 아니였네요. 정말 다르게 보이고 머리가 멍하네요. 그리고 제가 가장 원했던게 함수선언식이나 함수 표현식이 아닌 보안 가능한 형식이 있는 코드를 작성하고 싶었는데 javascript도 패턴이 있다는 것을 알게 되었습니다. 이고잉님도 제로초님도 너무 감사합니다.
6년 전
네임스페이스 단점 해결방법 코드에서
return { y: y }; \u003c-- 오타가 있는것 같습니다.
-> return {y: y() };
6년 전
오타가 아닙니다~ 다시 한 번 잘 살펴보세요~
6년 전
return에 의해 { y: function () { alert(x) } }; 가 newScope에 저장됩니다 이 부분에서 alert(x)는 alert 되지 않나요??
6년 전
alert는 y가 호출될 때 실행됩니다.
6년 전
자바스크립트를 야매로배워서 여러가지 이해가 안가는부분이많았는데 대부분 이블로그에서 배웠네요 정말 감사합니다.
그런데 개인적인 의견인데 유튜브 채널이름을 바꾸시는건 어떨까요 조현영 검색하니 어떤 댄서분인가 가수분만 나와서 다시 웹페이지로 돌아와서 링크타고 들어갔네요.
6년 전
제로님 안녕하세요... 스코프, 실행컨텍스트 와 같은 개념들을 제로님 블로그를 보고서야 이해를 하게 되었습니다..... 제 인생의 은인이십니다. 정말 너무 감사합니다. 제로님의 글을 읽고 그동안 읽은 다른 글들을 읽으니까 모두 이해가 되었습니다.
6년 전
극찬이시네요. 감사합니다 ㅠㅠ
6년 전
안녕하세요. 글 잘읽었습니다.
질문이 있는데,

var obj = {
x: 'local',
y: function() {console.log(this.x)}
}
obj.y() 하면 'local'이 출력되는데요.

저부분에서 this는 윈도우를 가르키는게 아닌가요?

var obj2 = {
c: this
}
obj2.c 하면 window가 출력되서 궁금하네요..
6년 전
저게 obj가 객체고 obj.y가 객체의 속성이잖아요? 속성이 함수인 경우를 메서드라고 하는데 그 경우에는 this가 자동으로 obj를 가리키게 됩니다.
7년 전
안녕하세요
스코프 체이닝에 질문드리고 싶은데요

참고 : 크롬 스크린샷 이미지 입니다.
https://i.stack.imgur.com/ORWye.jpg


function test1() {

function test2() {

function test3() {

function test4() {
console.log("test4")
}
debugger;
test4();
}
test3();
}
test2();
}
test1();


이 코드를 실행하고..
크롬 디버깅으로 확인했을때요..


scope가 왜 1 일까요???
scope 4가 되어야 하는게 아닌가요? 아래처럼요..

scope[1] // test3
scope[2] // test2
scope[3] // test1
scope[4] // global
7년 전
[[scope]]는 스코프 체인이 아니라 현재 참조하고 있는 스코프를 의미합니다. 다음 코드와 비교해보세요.

function test1() {
function test2() {
function test3() {
var x = 1;
function test4() {
console.log("test4", x)
}
debugger;
test4();
}
test3();
}
test2();
}
test1();

전체 스코프체인 중에 참조하는 것만 [[scope]]에 기록되는 거라고 생각하시면 됩니다.
7년 전
window강좌 로드가 안되는거같아요~~
7년 전
오늘 무슨 일인지 서버가 너무 느리네요 ㅠ
7년 전
좋은 글 감사합니다
질문이 있는데 랙시컬 스코핑 위에 스코프
체인 예제 함수에서 전역스코프에서 name 변수 값으로 zero 를 얻었고 만약 전역 스코프에도 없으면 undefind 값을 반환한다는데 전역스코프에도 name 이 없으면 undefind가 아닌 name을 찾지 못했다는 에러가 나타나는게 아닌가요?? 궁금합니다
7년 전
그렇네요! 수정하겠습니다
7년 전
스코프 체인 예제 중에 오타가 있어서 SyntaxError나길래 알려드립니다.
funciton >> function
7년 전
감사합니다. ZeroCho Blog 페이스북 메시지 보내주시면 문화상품권 5000원 하나 보내드리겠습니다.