[모던JS: Core] 함수 심화 (3)

KG·2021년 5월 15일
0

모던JS

목록 보기
9/47
post-thumbnail

Intro

본 포스팅은 여기에 올라온 게시글을 바탕으로 작성되었습니다.
파트와 카테고리 동일한 순서로 모든 내용을 소개하는 것이 아닌, 몰랐거나 새로운 내용 위주로 다시 정리하여 개인공부 목적으로 작성합니다.
중간중간 개인 판단 하에 필요하다고 생각될 시, 기존 내용에 추가로 보충되는 내용이 있을 수 있습니다.

오래된 var

자바스크립트에서 변수는 세가지 방식으로 선언이 가능하다.

  1. var
  2. let
  3. const

앞서서 2번과 3번의 경우는 많은 예시를 통해 살펴보았다. 그러나 1번의 경우는 이때까지 사용하지 않았는데, 이는 2번과 3번 예약어가 도입되기 전까지 오래된 자바스크립트에서 사용하던 방식이기 때문이다.

단순히 오래되었기 때문에 사용을 하지 않는 것은 아니다. varlet으로 선언한 변수와 유사하게 작동하지만 내부 동작 방식은 차이가 있다. letconst 키워드 역시 ES6(ES2015)에 도입되었다.

1) 블록스코프가 없는 var

var로 선언한 변수는 항상 함수 스코프이거나 전역 스코프이다. 따라서 블록 밖에서 해당 변수에 접근이 가능하다.

if (true) {
  var test = true;
}

console.log(test);	// true

for (var i = 0; i < 10; i++) {
  ...
}
  
console.log(i);	// 10
  
// 코드 블록이 함수 안에 있다면,
// var은 함수 레벨 변수로 작용
function sayHi () {
  if(true) {
    var phrase = 'hello';
  }
  
  console.log(phrase);
}
  
console.log(phrase);	// Error

2) 재선언 가능

let으로 선언한 변수는 값을 갱신할 수는 있지만 같은 이름으로 다시 선언하는 것이 불가하다.

let user;
let user = 'kg';	// SyntaxError

그러나 var로 선언한 변수는 이러한 재선언이 자유롭다.

var user;
var user = 'kg'	// 'kg'

크롬 개발자 도구에서는 let 키워드로 선언한 변수여도 재선언이 가능한데, 이는 개발자 도구가 비엄격 모드이기 때문이다. use stirct와 함께 실행하면 에러가 뜨는 것을 확인할 수 있다.

3) 선언 전 사용 가능

var 선언은 함수가 시작될 때 처리된다. 즉 변수가 선언 위치와 상관 없이 함수 본문이 시작되는 지점에서 정의된다. 이처럼 변수가 끌어올려 지는 현상을 호이스팅 (hoisting)이라고 한다. 단 변수가 중첩 함수 내에서 재정의 되지 않아야 이 규칙이 적용된다.

function sayHi () {
  phrase = 'Hello";
  
  console.log(phrase);
  
  var phrase;	// 마지막에 선언되었지만
  // 위치 상관 없이 맨 위로 끌어올려 짐(호이스팅)
}

function sayHi () {
  phrase = 'Hello';
  
  // var은 블록스코프를 무시하기에
  // 정상 실행됨
  if (false) {
    var phrase;
  }
  
  console.log(phrase);
}

이때 주의점은 선언은 호이스팅이 되지만, 할당은 호이스팅이 되지 않는다는 점이다. 할당은 실행 흐름이 해당 코드에 도달했을 때 처리된다.

function sayHi() {
  console.log(phrase);	// undefined
  
  var phrase = "hello";
}

// 위 함수는 아래와 동일하다.

function sayHi() {
  var phrase;	// 1. 선언부는 호이스팅
  
  console.log(phrase);	// 3. 따라서 이 시점에서는 undefined
  
  phrase = "hello";	// 2. 할당은 실행 흐름이 도달했을 때 수행
}

4) 즉시 실행 함수 표현식(IIFE)

ES6 이전에는 var만 사용할 수 있었지만 블록 레벨 수준이 아닌 까닭에 블록 레벨을 사용하기 위한 여러 가지 방법을 고안하게 된다. 이때 만들어진 가장 대표적인 방식이 즉시 실행 함수 표현식(Immediately-Invoked Function Expression)이다. var의 한계 극복을 위해 고안된 방법에다 var 역시 과거의 잔재인만틈 오늘날 많이 쓰이지는 않는다.

IIFE는 함수를 보통 괄호로 감싸서 만들 수 있다. 이와 같이 함수를 생성하면 자바스크립트가 함수를 함수 선언문이 아닌 표현식으로 인식하도록 속일 수 있다. 자바스크립트에서는 함수 선언문으로 함수를 만들 땐 반드시 함수 이름이 있어야 한다. 또한 함수 선언문으로 정의한 함수를 정의와 동시에 호출하는 것을 허용하지 않는다. 그러나 함수 표현식은 이름이 없어도 괜찮고, 즉시 호출도 가능하다.

// 선언과 동시에 즉시 실행
(function () {
  var msg = 'hello';
  
  console.log(msg);	// 'hello'
})();

console.log(msg); // msg는 함수 실행이 끝났기 때문에 접근 불가

오늘날엔 변수가 블록 레벨 스코프를 갖기 위해 IIFE를 사용하기 보다는 let 또는 const를 더 많이 사용한다.

전역 객체

자바스크립트는 브라우저 환경에서는 전역 객체가 window, Node.js 환경에서는 보통 global 이라고 칭한다. 각 호스트 환경마다 부르는 이름은 다를 수 있다.

최근 자바스크립트 명세에 전역 객체의 이름을 globalThis로 표준화하자는 내용이 추가되었지만, 크로미움 기반이 아닌 몇몇 브라우저는 아직 이를 지원하지 않는다. (사용을 위해선 Polypill 필요)

보통 브라우저에서 let이나 const가 아닌 var로 선언한 전역 함수 또는 전역 변수는 전역 객체의 프로퍼티가 된다.

var globalVar = 5;

console.log(globarVar);		// 5
console.log(window.globarVar);	// 5

하위 호환성을 이유로 위와 같은 방식이 아직 남아있지만 권장되지는 않는다. 만일 중요한 변수라서 모든 곳에서 사용할 수 있도록 전역변수화 하려면, 아래와 같이 전역 객체에 직접 프로퍼티를 추가하는 방법을 추천한다.

window.currentUser = {
  name: 'John'
}

console.log(currentUser.name);		// 'John'
console.log(window.currentUser.name);	// 'John'

전역객체를 이용해 최신 자바스크립트가 현재 브라우저에 지원되는지 파악할 수 있다. 나아가 미지원되는 경우엔 직접 폴리필을 구성할 수 있다.

if (!window.Promise) {
  console.log('구식 브라우저 사용중...');
  
  window.Promise = ... // Promise 기능 구현...
}

객체로서의 함수

자바스크립트에서 함수는 값으로 취급되며, 이때 자료형은 객체형이다. 즉 함수는 호출이 가능한 행동 객체라고 이해할 수 있다. 이때 함수는 결국 객체의 일종이기 때문에 호출뿐만 아니라 객체와 같이 함수 자체에 프로퍼티를 추가하거나 참조를 통해 전달이 가능하다.

1) name 프로퍼티

name 프로퍼티를 사용하면 함수의 이름에 접근할 수 있다. 이름 할당 로직은 익명 함수라던가 기본값을 사용하는 경우에도 알아서 자동으로 할당한다. 이 기능을 contextual name이라고 부른다.

function sayHi () {
  console.log('hi');
}

console.log(sayHi.name);	// sayHi

let sayHi = function () {
  console.log('hi');
}

console.log(sayHi.name);	// sayHi

function f(sayHi = function() {}) {
  console.log(sayHi.name);
}

f();	// sayHi

2) length 프로퍼티

내장 프로퍼티인 length는 함수 매개변수의 개수를 반환한다. 이를 통해 다른 함수 안에서 동작하는 함수의 타입을 검사할 때 활용할 수 있다. 즉 다형성(polymorphism) 구현에 이용할 수 있다.

function f1(a) {}
function f2(a, b) {}
function f3(a, b, c) {}
function f4(a, b, c, ...rest) {}

console.log(f1.length);	// 1
console.log(f2.length);	// 2
console.log(f3.length);	// 3
console.log(f4.length);	// 3 => 나머지 매개변수는 개수에 포함 X
// 다형성 예시
function ask(question, ...handlers) {
  let isYes = confirm(question);
  
  for(let handler of handlers) {
    if (handler.length === 0) {
      if (isYes) handler();
    } else {
      handler(isYes);
    }
  }
}

// 사용자가 OK를 클릭한 경우, 핸들러 두 개를 모두 호출함
// 사용자가 Cancel을 클릭한 경우, 두 번째 핸들러만 호출함
ask("질문 있으신가요?", () => alert('OK를 선택하셨습니다.'), result => alert(result));

3) 커스텀 프로퍼티

함수에 자체적으로 프로퍼티를 추가할 수 있다. 하지만 프로퍼티는 변수가 아님에 주의해야 한다. 프로퍼티를 저장하는 것처럼 함수를 객체와 유사하게 다룰 수는 있지만, 실행에 아무런 영향을 끼치지 않는다. 변수는 함수 프로퍼티가 아니고, 함수 프로퍼티는 변수가 아니기 때문에 둘 사이에 공통점은 없다.

function makeCounter() {
  function counter() {
    return counter.count++;
  }
  
  counter.count = 0;
  
  return counter;
}

const counter = makeCounter();

counter();	// 0
counter();	// 1

이 경우 count는 지역변수가 아니고 따라서 외부 렉시컬 환경이 아닌 함수 프로퍼티에 바로 저장이 된다. 따라서 클로저 패턴과 달리 count는 외부에서 값에 접근과 수정이 가능하다.

...

counter.count = 10;
counter();	// 10

기명 함수 표현식

기명 함수 표현식(Named Function Expression, NFE)은 이름이 있는 함수 표현식을 나타내는 용어이다.

let sayHi = function func (who) {
  console.log(`Hello ${who}`);
};

이처럼 함수 표현식에 쓰이는 함수에 이름을 붙이면 다음과 같은 두 가지 변화가 생긴다.

  1. 이름을 사용해 함수 표현식 내부에서 자기 자신 참조 가능
  2. 기명 함수 표현식 외부에선 그 이름 사용 불가
let sayHi = function func (who) {
  if (who) {
    console.log(`Hello, ${who}`);
  } else {
    func("Guest");	// 자신을 다시 호출
  }
};

sayHi();	// Hello, Guest

// 그러나 기명 함수 표현식 밖에서는 이름에 접근 불가
func();		// Error, func is not defined

이때 다음과 같은 의문이 들 수 있다. 재귀 함수에서처럼 자기 자신을 직접 호출해도 되지 않을까? 물론 가능하다.

let sayHi = function func (who) {
  if (who) {
    console.log(`Hello, ${who}`);
  } else {
    sayHi("Guest");	// 자신을 다시 호출
  }
};

그러나 이 경우에는 외부 코드에 의해 sayHi가 변경될 수 있는 문제가 생긴다. 아래와 같이 sayHi의 참조가 끊기게 되면 중첩 호출이 불가하다. 왜냐하면 함수가 sayHi를 자신 외부 렉시컬 환경에서 가져오기 때문이다. 지역 레시컬 환경에는 sayHi가 없기 때문에 외부 렉시컬로 접근하는데 이때 호출 시점에서는 sayHi의 참조가 null로 끊겨 있기 때문에 에러가 발생한다.

let sayHi = function func (who) {
  if (who) {
    console.log(`Hello, ${who}`);
  } else {
    sayHi("Guest");	// 자신을 다시 호출
  }
};

let welcome = sayHi;
sayHi = null;

welcome();	// 중첩 호출 불가!

이때 기명 함수 표현식을 이용하면 위의 에러를 방지할 수 있다. 기명 함수 표현식은 함수 지역 수준에 존재하기 때문에 외부 렉시컬 환경에서 찾지 않기 때문이다. 따라서 외부에서 변경이 되더라도 문제 없이 함수 표현식 내부에서 자신을 호출할 수 있다.

함수 선언문 방식으로는 내부 이름 지정이 불가하다. 기명 함수 표현식 처럼 내부 이름 지정은 오직 함수 표현식 방식에서만 가능하다.

new Function

함수 표현식과 함수 선언문 외에도 함수를 만들 수 있는 방법이 있다. new Function 문법으로도 함수를 생성할 수 있다. 보통 흔히 사용되는 방법은 아니지만 이 방법 외에 대안이 없을 경우에 사용한다.

// 2개의 인수 전달 (a, b)
const sum = new Function('a', 'b', 'return a + b');
sum(1, 2);	// 3

// 전달하는 인수 없음
const sayHi = new Function('console.log("hi")');
sayHi();	// hi

기존 방식과 가장 큰 차이점은 런타임에 받은 문자열을 사용해 동적으로 함수를 생성할 수 있다는 점이다. new Function을 통해서 어떤 문자열도 함수로 바꿀 수 있다. 서버에서 코드를 받거나 템플릿을 사용해 함수를 동적으로 컴파일 해야하는 경우 등 활용할 수 있다.

앞서서 자바스크립트는 모든 함수가 클로저가 될 수 있다고 했다. 그러나 엄연히 구분하면 new Function으로 생성된 함수는 클로저가 될 수 없다. 왜냐하면 해당 방식을 생성된 함수는 [[Environment]] 프로퍼티가 현재 렉시컬 환경이 아닌 전역 렉시컬 환경을 참조하기 때문이다. 즉 new Function을 이용해 만든 함수는 외부 변수에 접근이 불가하고 오직 전역 변수에만 접근할 수 있다.

function getFunc() {
  let value = 'test';
  
  let func = new Function('console.log(value)');
  
  return func;
}

getFunc()();	// ReferenceError

이는 흔히 자바스크립트가 프로덕션 환경에서 배포될 때, 번들러 또는 압축기를 통해 파일이 압축되면서 지역 변수 이름이 짧은 네이밍으로 변환되게 되는데 때문에 new Function 문법으로 만든 함수 내부에서 외부 변수 접근 시에 변수가 이미 다른 이름으로 변경되었기 때문에 찾을 수가 없는 것이다. 이는 단점 같아 보이는 특징이기도 하지만, 에러를 예방하는 관점에서는 장점이 되기도 한다. 구조상 매개변수를 사용해 값을 전달받는게 더 좋을때도 있고, 함수 내부에서 외부 변수에 접근하는 것은 때때로 아키텍처 관점에서 좋지 않고 에러에 취약한 경우가 더러 있기 때문이다.

References

  1. https://ko.javascript.info/advanced-functions
profile
개발잘하고싶다

0개의 댓글