[JavaScript Deep Dive] 12. 함수

소정·2023년 12월 17일
1
post-thumbnail

아무래도 ... 1일 1모던자스는 좀 어려울 것 같다 .. 나으ㅣ 최대의 장점 ! 융통성을 살려보겠다. ^____ ____^
원래는 멋사 수업이 html, css를 나가는 동안에는 1주 1회 포스팅을 하려고 했는데, 생각보다 새롭게 알게되어 정리해야 될 내용이 많기 때문에 멋사 수업을 우선하여 포스팅하는 것으로 계획 변경 !
모던 자스는 1주일에 2-3회 (부디) 뿌시러 와주겠다. 그런데 마침 함수 파트가 짱짱 길다. (ㅎ) 그래도 가보자고 !!!!!!!


1. 함수란?

자바스크립트의 함수는 수학에서 사용되는 함수의 개념과 비슷하다.
조금 더 익숙한 수학의 함수를 먼저 살펴보자 !

수학의 함수

  • 입력을 받아 출력을 내보내는 일련의 과정
  • f(x , y) = x + y
  • f(2 , 5) = 7

위의 함수에 '재료(2, 5)'를 투입하면 제품을 '생산/출력(7)'한다. x와 y는 내부로 입력을 받아들이는 변수이고 2와 5는 입력, 7은 함수의 실행 결과인 출력이다. 입력인 2와 5는 입력을 받아들이는 변수 x와 y를 통해 함수 외부에서 내부로 전달된다. 또 함수의 실행 결과인 출력은 내부에서 외부로 반환된다.

자바스크립트의 함수

위의 수학의 함수를 자스의 함수로 변경하면 아래의 코드와 같다.

// 함수 정의
function add(x,y){
  return x + y;
}
// 함수 호출
add(2,5); // 7
  • 함수 : 일련의 과정을 문으로 구현하고, 코드 블록으로 감싸서 하나의 실행 단위로 정의한 것

자스의 함수도 수학의 함수와 동일하게, 입력을 받아 출력을 내보낸다.
그 과정의 개념을 자스의 용어로 풀이하면 다음과 같다.

  • 매개변수 함수 내부로 입력을 전달받는 변수
  • 인수 입력
  • 반환값 출력
  • 함수 이름 함수를 구별하기 위한 식별자

인수가 매개변수를 통해 함수 내부로 전달되는 '함수 호출'이 명시적으로 있어야 정의된 함수가 실행될 수 있다.


2. 함수를 사용하는 이유

  1. 코드의 재사용
  2. 유지보수 편의성
  3. 코드의 신뢰성
  4. 코드의 가독성

3. 함수 리터럴

리터럴 : 값을 생성하기 위한 표기법
함수는 객체 타입의 값인데, 함수 리터럴로 생성할 수 있다. 함수 리터럴은 아래 목록으로 구성된다.

  • function 키워드
  • 함수 이름
  • 매개 변수 목록
  • 함수 몸체
// 변수에 함수 리터럴을 할당함 (함수 리터럴: 표현식인 문)
var f = function add(x, y) {
  return x + y;
}

함수 이름

  • 식별자이고, 식별자 네이밍 규칙 준수 필요
  • 함수 몸체 내에서만 참조가 가능한 식별자
  • 생략 가능
    ㄴ 생략 O : 무명/익명 함수
    ㄴ 생략 X : 기명 함수

매개변수 목록

  • 0개 이상의 매개 변수를 (소괄호)에 작성하고 쉼표로 구분함
  • 함수 호출시 지정한 인수가 순서대로 할당됨. * 순서 중요함
  • 함수 몸체 내에서 변수와 동일하게 취급됨. 따라서 식별자 네이밍 규칙 준수 필요

함수 몸체

  • 함수 호출시, 일괄적으로 실행될 문들을 하나의 실행 단위로 정의한 코드 블록
  • 함수 호출에 의해 실행됨

함수는 조금 특별한 객체라고 할 수 있다. 일반 객체는 호출이 불가능하지만, 함수는 호출이 가능하다. 또한 함수는 고유한 프로퍼티를 갖는다. 함수가 객체라는 사실은 자스에서 아주 중요한 특징인데, 이것은 18장에서 조금 더 자세히 알아볼 예정이다.


4. 함수 정의

함수 정의란, 함수 호출 이전에 매개변수와 실행문, 반환값을 지정하는 것을 뜻한다. 함수 선언이 아님 주의 !
함수가 정의되면, 자바스크립트 엔진에 의해 평가되고 함수 객체가 된다. 함수를 정의하는 방법은 총 4가지가 존재한다.

  • 함수 선언문
  • 함수 표현식 (함수 리터럴로 생성된 함수 객체를 변수에 할당하는 방식)
  • Function 생성자 함수
  • 화살표 함수(ES6)

1. 함수 선언문

함수 선언은 함수 리터럴 형태(Function 키워드 + 이름 + 매개변수 + 몸체)와 동일하다. 하지만, 함수 이름을 생략할 수 있는 함수 리터럴과 다르게 함수 선언은 함수 이름을 생략할 수 없다.

// 함수 선언
function add(x,y) {
  return x + y; 
}
// 함수 참조 (다만, node.js 환경에서는 console.log와 같이 결과가 출력됨)
console.dir(add); // f add(x, y)
// 함수 호출
console.log(add(2, 5)); // 7

함수 선언문은 표현식이 아닌 문이다.

  • : 프로그램을 구성하는 기본 단위이자 최소 실행 단위
  • 표현식 : 값으로 평가될 수 있는 문

즉 함수 선언문은 값으로 평가될 수 없다. 함수 선언문을 개발자 도구에서 실행하면 완료 값으로 undefined가 출력된다. 표현식이 아니기 때문에 변수에 할당할 수 없다.

하지만 우리는, 위의 3. 함수 리터럴에서 함수를 변수에 할당했다 ... ! 이것은 기명 함수 리터럴이 중의적으로 사용되기 때문이다. 아래 예시를 살펴봐보자!

중의적인 표현 : { }

자바스크립트에서 { }는 블록문일 수도 객체 리터럴일 수도 있다. 자바스크립트 엔진은 두가지를 어떻게 구분할까? 바로 코드의 맥락에 따라 해석이 달라진다. { }가 단독으로 존재하면 자바스크립트 엔진은 중괄호를 블록문으로 해석한다. 또한 { }가 평가되어야 할 문맥에서 피연산자로 사용된다면 객체 리터럴로 해석한다. 이처럼 자바스크립트에는 중의적인 표현이 존재하고, 자바스크립트 엔진은 코드의 문맥에 따라 해석을 달리한다.

중의적인 표현 : 기명 함수 리터럴

{ }와 동일하게 함수 이름이 있는 함수 리터럴도 중의적인 표현으로 코드의 맥락에 따라 해석된다.
두 가지 모두 공통적으로 함수가 생성되지만, 함수를 생성하는 내부 동작에는 차이가 존재한다.
또한 함수 선언문은 함수 이름을 생략할 수 없지만, 함수 리터럴은 생략이 가능하다.

  • 함수 선언문으로 해석되는 경우 : 피연산자로 사용되지 않고 단독으로 사용
  • 함수 리터럴로 해석되는 경우 : 값으로 평가되어야 하는 문맥(변수 할당, 피연산자로 사용)

    함수 선언문(표현식이 아닌 문)과 다르게, 함수 리터럴은 표현식인 문이다.
    그렇기 때문에 변수에 할당이 가능하고 피연산자로도 사용될 수 있다.

함수 리터럴 vs 함수 선언식 : '호출'의 차이

// 함수 선언식 : 단독으로 사용되어 함수 선언식으로 해석됨
function foo(){console.log('foo');}
foo(); // foo

// 함수 리터럴 : ( ) 연산자의 피연산자로 사용되어 기명 함수 리터럴로 해석됨
( function bar(){console.log('bar');} )
bar(); // ReferenceError: bar is not defined

중의적인 의미로 사용되는 기명 함수 리터럴은 코드 문맥에 따라 함수 선언문으로도, 함수 리터럴 표현식으로도 해석된다. 위의 예시를 보면, 함수가 생성되었다는 점은 동일하지만 함수 호출에는 차이점을 보인다.

왜냐하면, 함수 리터럴함수 이름함수 내부에서만 참조할 수 있는 식별자이기 때문이다. 그렇기 때문에 함수 몸체 외부에선 함수 이름 'bar'로 함수를 호출할 수 없다. 즉, bar 함수는 호출할 수 없다는 뜻이다.

반대로 함수 선언문으로 정의된 함수는 'foo'로 호출되었다. 분명 foo는 함수 내부에서만 유효하고, 변수 이름을 할당한 적도 없는데 호출이 불가능한데 어떻게 호출이 가능했을까? 함수 선언문은 표현식이 아닌 문이기 때문에 변수 할당이 불가능하다.원인은 자바스크립트 엔진이 암묵적으로 생성한 식별자 덕분이다.

암묵적으로 생성된 식별자

함수가 정의되면, 자바스크립트 엔진은 함수 선언문을 해석하여 함수 객체를 생성한다.

  • 사용자 : 함수 정의
  • 자바스크립트 엔진 : 함수 선언문 해석 -> 함수 객체 생성

생성된 함수 객체를 호출하기 위해선 그 함수 객체를 가리키는 식별자가 필요하다. (함수이름: 함수 내부에서만 유효함) 결국, 정의된 함수를 호출하기 위해서 자바스크립트 엔진은 암묵적으로 함수 이름과 동일한 식별자를 생성하여 함수 객체에 할당한다.

이 과정을 다시 한번 살펴보면, 우리는 함수 이름으로 함수를 호출하는 것이 아니라 함수 객체를 가리키는 (암묵적으로 자바스크립트 엔진이 생성하여 할당한) 식별자로 호출한다.

사실 위의 이미지에서 확인한 코드는 다음에 나오는 함수 표현식이다. 결국, 자바스크립트 엔진은 함수 선언문을 함수 표현식으로 변환하여 함수 객체를 생성하는 것이다. 하지만, 함수 선언문과 함수 표현식이 정확히 동일하게 작동하는 것은 아니다.

2. 함수 표현식

일급 객체

일급 객체란 객체 타입의 함수가 값처럼 변수에 할당하고, 프로퍼티의 값으로 활용하고, 배열의 요소도 될 수 있는 객체라는 뜻이다. 즉 함수가 일급 객체라는 뜻은, 함수를 값처럼 자유롭게 사용할 수 있다는 의미다.

이때 함수 리터럴로 생성된 함수 객체를 변수에 할당하는 함수 정의 방식을 함수 표현식이라고 한다.

// 함수 선언문
function add(x,y) {
  return x+y;
};
// 함수 표현식
var add = function (생략)(x,y) {
  return x+y;
};

함수 리터럴의 함수 이름은 생략이 가능하다. (함수 선언문으로 생성된 함수와의 차이점) 함수 표현식의 함수 리터럴은 일반적으로 함수 이름을 생략한다. 함수를 호출할 땐, 위의 함수선언문에서 확인했던 내용과 마찬가지로 함수 몸체 내부에서만 유효한 함수 이름이 아닌, 함수 객체를 가리키는 식별자를 사용해야 한다.

// 기명 함수 표현식
var add = function foo (x,y) {
  return x + y;
}

// 함수 객체를 가리키는 식별자(add)로 호출
console.log(add(2,5)); // 7

// 함수 이름(foo)으로 호출
console.log(foo(2,5)); // ReferenceError: foo is not defined

3. 함수 생성 시점과 함수 호이스팅

// 함수 참조
console.dir(add); // f add(x,y)
console.dir(sub); // undefined

// 함수 호출
console.log(add(2, 5)); // 7
console.log(sub(2, 5)); // TypeError: sub is not defined

// 함수 선언문
function add(x,y){ return x + y; }
// 함수 표현식
var sub = function (x,y){ return x - y; }

위의 예제를 확인하면, 함수 선언 이전에 호출이 가능한 함수 선언문과 함수 정의 이전엔 호출이 불가능한 함수 표현식의 차이점을 확인할 수 있다. 이 차이점은 함수 선언문과 함수 표현식의 생성 시점이 다르기 때문이다.

함수 호이스팅

자바스크립트의 모든 선언문은 런타임(코드가 한 줄씩 순차적으로 실행되는 시점) 이전에 자바스크립트 엔진에 의해 먼저 실행된다. 즉 함수 선언문으로 함수를 정의하면, 런타임 이전에 함수 객체가 먼저 생성된다. 그리고 자바스크립트 엔진은 함수 이름과 동일한 식별자를 암묵적으로 생성, 생성된 객체를 할당한다.

결국 코드가 순차적으로 실행되는 런타임에는 이미 함수 객체가 생성되어 있고 함수 이름과 동일한 식별자까지 할당이 완료된 상태이다. 그렇기 때문에, 함수 선언문 이전에 함수를 참조, 호출할 수 있다. 함수 선언문이 코드의 선두로 끌어 올려진 것처럼 동작하는 자스의 고유 특징을 함수 호이스팅 이라고 한다.

변수 호이스팅

함수 호이스팅과 미묘한 차이가 있는 변수 호이스팅에 대해 알고 갈 필요가 있다. var 키워드로 선언한 변수는 런타임 이전에 변수가 선언되어 undefined로 초기화된다. 그리고 런타임에 변수에 값이 할당된다.

  • 공통점 자스 엔진에 의해 런타임 이전에 먼저 실행되어 식별자를 생성
  • 차이점 변수는 undefined로 초기화되고, 함수 선언문의 식별자는 함수 객체로 초기화 됨
    변수 선언문 이전에 변수 참조 => 변수 호이스팅에 의해, undefined로 평가
    함수 선언문 이전에 함수 호출 => 함수 호이스팅에 의해, 호출 가능

결국, var 키워드를 사용한 변수 선언문 이전에 변수를 참조하면 undefined로 평가되어 참조가 불가능하다. 반면에 함수 선언문으로 정의한 함수는 정의 이전에 호출이 가능하다.

함수 표현식 = 변수 선언문 + 변수 할당문(런타임에 실행)

함수 표현식은 함수 호이스팅이 적용되는 함수 선언문과 차이점이 있다. 함수 표현식은 변수에 할당되는 값이 함수 리터럴인 문이다. 이것은 변수 선언문변수 할당문한 번에 기술한 축약 표현과 동일하게 동작한다는 것이다.

변수 선언은 위에서 살펴본것과 동일하게, 런타임 이전에 실행되어 undefined로 초기화된다. 하지만 변수 할당문할당문이 실행되는 시점인 런타임에 평가된다. 변수 선언과 변수 할당의 축약 표현과 동일하게 작동하는 함수 표현식은 결국 undefined로 초기화된 이후, 런타임에 함수 객체로 평가된다.

  • 함수 표현식 : 변수 호이스팅 발생 (런타임에 함수 객체로 평가)
    ㄴ 함수 표현식 이전에 함수 호출 > undefined를 호출한 것으로 TypeError
  • 함수 선언문 : 함수 호이스팅 발생 (런타임 '이전에' 함수 객체로 평가)
    ㄴ 함수 선언문 이전에 함수 호출 > 함수 객체를 호출한 것으로 호출 가능

권장되는 방식

함수 호이스팅은 함수 호출 전에 함수를 선언해야 한다는 규칙을 무시한다. 이러한 이유로, JSON을 창시한 더글라스 크락포드는 함수 선언문 대신 함수 표현식을 사용할 것으로 권장한다.

가독성의 측면에서 함수 선언문을 스크립트 최하단에 한번에 작성 후, 선언 이전에 함수를 호출했었다. 예기치 못한 상황을 방지하기 위해 ! 더 권장되는 함수 표현식과 화살표 함수를 사용하도록 노력해야겠다.

여기까지 벌써 이틀째 함수에 대해 공부중이다 .. ! 정말 긴 내용이지만, 그만큼 중요하고 그동안 명확하지 않았던 개념이 정리되어 상당히 뿌듯하다. 꾸준히 공부 가보자고 ~

4. Function 생성자 함수

  • 생성자 함수 : 객체를 생성하는 함수, 17장에서 자세히 다룰 예정
// 함수 생성자 함수 정의
function MyFunction(name) {
  this.name = name; // 함수의 속성으로 이름을 설정
  this.sayHello = function() {
    console.log('Hello, ' + this.name + '!');
  };
}

// 함수 생성자를 사용하여 객체 생성
var myObject = new MyFunction('John');

// 객체의 메서드 호출
myObject.sayHello(); // 출력: Hello, John!

이 방법으로 함수를 생성하는 방식은, 일반적이지도 바람직하지도 않은 방법이라 권장되지 않는다. 그래도 조금만 알아보자면 ! 자스가 기본으로 제공하는 빌트인 함수인 Function 생성자 함수에 매개변수 목록, 함수 몸체를 문자열로 전달한다. 이때 new 연산자(생략 가능)와 함께 호출하면 함수 객체를 생성해서 반환한다.

Function 생성자 함수가 권장되지 않는 이유는 2가지 정도이다. 우선 Function 생성자 함수는 클로저를 생성하지 않는다. 그리고 이 함수는 함수 선언문/함수 표현식으로 생성한 함수와 다르게 동작한다. 우선은 이정도만 알고 넘어가도 괜찮다.

5. 화살표 함수

const multiply = (a, b) => a * b;

ES6에서 도입된 함수로, function 키워드 대신 화살표(=>)를 사용해 함수를 간략한 방법으로 선언할 수 있다. 화살표 함수는 항상 익명 함수로 정의한다.

이 화살표 함수는 함수선언문/함수표현식을 대체하기 위해 디자인 된 것이 아니다. 표현이 간략한 만큼 내부 동작 또한 간략화 되었다. 그렇기 때문에 발생하는 함수선언문/함수표현식과의 차이점이 아래와 같다. 아직 학습하지 않은 내용만 냅다 있어서 아래 내용을 모두 살펴본 후, 26장에서 자세히 살펴 볼 예정.

  • 화살표 함수는 생성자 함수로 사용할 수 없음
  • 기존 함수와 this 바인딩 방식이 다름
  • prototype 프로퍼티가 없음
  • arguments 객체를 생성하지 않음

5. 함수 호출

함수는 함수를 가리키는 식별자함수 호출 연산자()로 호출한다. 함수를 호출하면, 현재의 실행 흐름을 중단!하고 호출된 함수로 실행 흐름을 옮긴다. 그 다음에 매개변수에 인수가 순서대로 할당되고, 함수 몸체의 문들이 실행된다.

1. 매개변수와 인수

함수 호출시 함수 내부로 전달되어야 하는 값이 있는 경우, 매개변수(인자)를 통해 인수를 전달한다.

매개변수(인자)

매개변수함수가 호출될 때마다 다음의 단계를 거친다.
1) 암묵적 매개변수 생성 2) undefiend로 초기화 3) 인수 순서대로 할당

  • 함수를 정의 할 때 선언
  • 함수 몸체 내부에서 변수와 동일하게 취급됨
  • 함수 몸체 내부에서만 참조가 가능하고, 함수 몸체 외부에선 참조할 수 없음
    ㄴ 매개변수의 스코프(유효 범위) : 함수 내부

인수

  • 인수는 함수를 호출 할 때 지정함
  • 인수는 표현식(값으로 평가가 가능한 문)이어야 함
  • 인수의 개수, 타입에 제한 없음

매개변수와 인수의 개수

함수는 매개변수의 개수와 인수의 개수가 일치하는지 확인하지 않는다.
즉 매개변수의 개수와 인수의 개수가 달라도 오류가 발생하지 않는다.

  • 매개변수 개수 > 인수 개수 할당되지 않은 매개변수의 값은 undefiend가 됨
  • 매개변수 개수 < 인수 개수 초과된 인수는 무시됨
    ㄴ 무시된 인수 : 암묵적으로 arguments 객체의 프로퍼티로 보관됨 (아래 예시 참고)

arguments 객체는 함수를 정의할 때 매개변수 개수를 확정할 수 없는 가변 인자 함수를 구현할 때 유용하게 사용된다. 하지만 이게 뭔지 잘 모르겠다. arguments 객체는 18장에서 더 자세히 ~

function add (x, y){
  console.log(arguments); // Arguments(3) [2,5,10, callee: F, Symbol(Symbol.iterator):F]
  return x + y;
}
add(2, 5, 10) // 매개변수 2개, 인수 3개 = 10이 무시됨

2. 인수 확인

function add (x, y){
  return x + y;
}

console.log(add(2)); // NaN
console.log(add('a','b')); // 'ab'

위와 같은 함수가 있다고 가정해보자. 함수를 정의한 개발자는 2개의 숫자 타입의 인수를 전달받아야 한다. 하지만 코드상으론 어떤 타입의 인수를 전달해야 하는지, 어떤 타입의 값을 반환해야 하는지 명확하지 않다. 그렇기 때문에 위의 2줄의 console.log로 함수를 호출했을 때 원하는 답을 얻지 못한다.

2줄의 console.log로 함수를 호출한 것은 문법상 어떤 문제도 없다. 하지만 원하는 답을 얻지 못했다.
그렇기 때문에, 함수를 정의할 때 적절한 인수가 전달되었는지 확인할 필요가 있다.

  • 자바스크립트 함수는 매개변수와 인수의 개수가 일치하는지 확인하지 않음
  • 자바스크립트는 동적 언어. 매개변수의 타입을 사전에 지정할 수 없음

인수확인방법 1. 타입스크립트

함수 내부에서 적절한 인수가 전달되었는지 확인하더라도, 부적절한 호출은 사전에 방지가 어렵다. 결국 런타임에 에러로 발생한다. 따라서 정적 타입 선언이 가능한 타입스크립트(자바스크립트 상위 확장) 등을 도입하여 컴파일 시점에 부적절한 호출을 방지할 수 있다.

인수확인방법 2. arguments 객체

arguments 객체를 통해 인수 개수를 확인할 수도 있다.

인수확인방법 3. 단축평가로 기본값 할당

인수가 전달되지 않은 경우, 단축 평가를 사용해 매개변수의 기본값을 할당할 수 있다.

function add(a, b, c){
  a = a || 0;
  b = b || 0;
  c = c || 0;
  return a + b + c;
}

console.log(add(1,2,3)); // 6
console.log(add(1,2)); // 3
console.log(add(1)); // 1
console.log(add()); // 0

인수확인방법 4. 매개변수 기본값

ES6에서 도입된 매개변수 기본값을 사용하면, 함수 내에서 수행하던 인수 체크 및 초기화를 간소화할 수 있다.
다만, 매개변수 기본값은 아래 상황에서만 유효하게 사용된다.

  • 매개변수에 인수를 전달하지 않았을 경우
  • 매개변수에 undefined를 전달한 경우
function add(a = 0, b = 0, c = 0){
  return a + b + c;
}

console.log(add(1,2,3)); // 6
console.log(add(1,2)); // 3
console.log(add(1)); // 1
console.log(add()); // 0

3. 매개변수의 최대 개수

매개변수의 최대 개수는 명시적으로 제한하고 있진 않다. 하지만 물리적 한계가 있으므로 자바스크립트 엔진마다 매개변수의 최대 개수에 대한 제한이 있겠지만, 충분히 많은 매개변수를 지정할 수 있다.

매개변수순서에 의미가 있다. 매개변수가 많아질수록 함수를 호출할 때 전달해야 할 인수의 순서를 많이 고려해야한다. 그렇게 되면, 함수의 사용법을 이해하기 어렵고 실수할 가능성이 높아진다. 만약 매개변수의 개수나 순서가 바뀌기라도 하면, 함수의 호출 방법도 바뀐다. 즉 코드 전체가 영향을 받는다는 뜻!

결국 매개변수는 함수 코드를 이해하는데 방해되는 요소라고 할 수 있다. 이상적인 매개변수는 0개이고 적을수록 좋다. 매개변수가 많다는 뜻은, 함수가 하는 일이 많다는 것을 의미한다. 함수는 한 가지 일만 해야하고 가급적 작게 만들어야 한다. 그렇기 때문에, 매개변수최대 3개를 넘지 않는 것을 권장한다.

매개변수가 3개 이상이라면, 객체로 전달하는 것이 유리하다. 다만, 함수 외부에서 전달된 객체를 함수 내부에서 변경할 경우 !! 부수효과가 발생하니 이 점을 주의해야한다. 12장에서 더 자세히 ~

  • 프로퍼티 키만 정확히 지정하면, 매개변수의 순서를 신경쓰지 않아도 됨
  • 명시적으로 인수의 의미를 설명하는 프로퍼티 키를 사용하여 코드의 가독성 향상

4. 반환문

  • 반환문 : return 키워드 + 표현식(반환값)
  • 함수 호출 : 반환값으로 평가됨

반환문을 사용해 실행 결과를 함수 외부로 반환할 수 있다. 이때 return 키워드를 사용하여, 자스에서 사용 가능한 모든 값을 반환할 수 있다. 또한 함수 호출표현식이다. 함수 호출 표현식은 return 키워드가 반환한 표현식의 평가 결과(반환값)로 평가된다.

반환문의 역할

  1. 반환문은 함수 실행을 중단하고 함수 몸체를 빠져나감

    • 반환문이 작성되어 있다면, 함수 실행이 중단되기 때문에 반환문 이후의 다른 문은 실행되지 않고 무시된다.
  2. 반환문은 return 키워드 뒤에 오는 표현식을 평가해 반환

    • 이때 return 키워드 뒤에 반환값으로 사용할 표현식을 명시하지 않으면 undefined 반환
    • 반환식은 생략이 가능. 이 경우, 마지막 문까지 실행 후 암묵적으로 undefined 반환
    • return 키워드와 표현식(반환값) 사이 줄바꿈이 있는 경우
      : 세미콜론 자동 삽입 기능에 의해 return ;이 됨 (undefined 반환)

이러한 역할을 하는 반환문은, 함수 내부에서만 사용이 가능하다. 함수 외부(전역)에서 반환문을 사용하면 문법 에러가 발생한다. (참고로 Node.js는 파일별 독립적인 파일 스코프를 갖기 때문에, 파일의 가장 바깥 영역에서 반환문을 사용해도 에러가 발생하지 않음)


6. '참조에 의한 전달'과 외부 상태의 변경

매개변수는 함수 몸체 내부에서 변수와 동일하게 취급된다.
즉 매개변수 또한 타입에 따라 값에 의한 전달(원시값)참조에 의한 전달(객체) 방식을 따른다.
매개변수를 함수 몸체에서 변경할 때, 매개변수의 타입에 따라 다른 결과가 도출된다.

'원시 타입' 인수를 전달받은 매개변수 변경

  • 원시값은 변경 불가능한 값
  • 원시 값 타입의 매개변수를 변경하면 '재할당'을 통해 할당된 원시 값을 새로운 원시 값으로 교체
  • 원시 타입 인수는 값 자체가 복사되어 매개변수에 전달되기 때문에, 함수 몸체에서 값을 변경해도 원본이 훼손되지 않음

'객체 타입' 인수를 전달받은 매개변수 변경

  • 객체는 변경 가능한 값
  • 직접 변경이 가능하기 때문에 재할당 없이 직접 '할당된 객체를 변경'
  • 객체 타입의 인수는 참조 값이 복사되어 매개변수에 전달된 것이기 때문에, 함수 몸체에서 값을 변경하면 원본이 훼손됨

즉 함수 외부에서 함수 몸체로 전달한 참조값에 의해 원본 객체가 변경되는 부수효과가 발생한 것이다.
이렇게 함수가 외부 상태를 변경하면, 상태 변화 추적이 어려워진다. 결국 코드가 복잡해지고 가독성을 잃는다.

이런 현상이 나타나는 원인은, 객체가 변경할 수 있는 값이고 참조에 의한 전달 방식으로 동작하기 때문이다. 객체의 변경을 추적하려면, 옵저버 패턴 등을 통해 객체를 참조하는 모든 이들에게 변경 사실 통보 및 추가 대응이 필요하다.

해결방법 : 불변 객체

객체를 불변객체로 만들면 위의 문제를 해결할 수 있다.
불변 객체를 만들면, 객체의 복사본을 새로 생성하는 비용이 들지만 객체를 변경 불가능한 원시 값처럼 동작하게 할 수 있다. 불변 객체로 만들어 객체의 상태 변경을 원천봉쇄할 수 있는 것이다.

객체의 상태 변경이 필요한 경우라면, 객체의 방어적 복사를 통해 원본 객체를 완전히 복제하고 깊은 복사를 통해 새로운 객체를 생성 및 재할당하여 교체할 수 있다. 이 방법을 사용하면, 부수효과를 방지할 수 있다.

순수 함수는 외부 상태를 변경하지 않고 의존하지도 않는 함수이다. 순수 함수를 통해 부수효과를 최대한 억제하여 오류를 피하고 안정성을 높이려는 프로그램 패러다임을 함수형 프로그래밍이라고 한다. 이 내용은 12장에서 자세히 다룰 예정.


7. 다양한 함수의 형태

1. 즉시 실행 함수

  • 함수 정의와 동시에 즉시 호출되는 함수
  • 단 한번만 호출되어 다시 호출 불가
  • 반드시 그룹 연산자 (...)로 감싸야 함
  • 값 반환, 인수 전달이 가능함

단 한번만 호출되어 다시 호출 불가

즉시 실행 함수는 일반적으로 익명 함수를 사용한다. 물론 이름이 있는 기명 즉시 실행 함수도 사용이 가능하다. 하지만 그룹 연산자 (...) 내의 기명 함수는 함수 선언문이 아닌, 함수 리터럴로 평가된다. 함수 이름이 함수 몸체에서만 참조 가능한 식별자로, 함수 이름이 있어도 즉시 실행 함수를 다시 호출할 수 없다.

// 익명 즉시 실행 함수
(function (){
  var a = 3;
  var b = 5;
  return a ** b;
}())

// 기명 즉시 실행 함수
(function foo(){
  var a = 3;
  var b = 5;
  return a ** b;
}());
foo(); // ReferenceError: foo is not defined

반드시 그룹 연산자 (...)로 감싸야 함

// 익명 즉시 실행 함수 
function (){ // SyntaxError : Function statements require a function name
  // ...
}()

// 기명 즉시 실행 함수 
function foo(){
  // ...
}(); // SyntaxError : Unexpected token ')'
// => function foo(){}; ();

익명 즉시 실행 함수를 그룹 연산자로 감싸지 않으면 에러가 발생한다.
이유는 함수 이름이 없는 함수 정의가 함수 선언문 형식에 맞지 않기 때문이다. 함수 선언문은 함수 이름을 생략할 수 없다. 그렇다면 기명 즉시 실행 함수는 어떨까?

기명 즉시 실행 함수도 역시 그룹 연산자로 감싸지 않으면 에러가 발생한다.
이유는 자바스크립트 엔진이 암묵적으로 수행하는 세미콜론 자동 삽입 기능 때문이다. 함수 선언문이 끝나는 위치, 즉 함수 코드 블록의 닫는 중괄호 뒤에 세미콜론이 암묵적으로 추가된다.

추가된 세미콜론으로 인해 function foo(){}; ();으로 해석된다. 중괄호 + 세미콜론 뒤의 ( ... )가 함수 호출 연산자가 아닌, 그룹 연산자로 해석되고 그룹 연산자에 피연산자가 없기 때문에 에러가 발생한다.

그룹 연산자의 피연산자는 값으로 평가된다. 기명/익명 함수를 그룹 연산자로 감싸면 함수 리터럴로 평가되어 함수 객체가 된다. 즉 그룹 연산자로 묶는 이유는, 먼저 함수 리터럴을 평가하여 함수 객체를 생성하기 위함이다. 함수 객체를 생성하기 위한 아래의 방법 중 가장 권장되는 방식은 1번이다.

( function (){ 
  // ...
}() );

( function (){ 
  // ...
}) ();

!function (){ 
  // ...
}();

+function (){ 
  // ...
}();

값 반환과 인수 전달이 가능함

// 값 반환
var res = (function () {
  var a = 3;
  var b = 5;
  return a * b;
}());
console.log(res); // 15

// 인수 전달
res = (function (a, b) {
  return a * b;
}(3, 5));
console.log(res); // 15

즉시 실행 함수도 일반 함수처럼 값을 반환할 수 있고 인수를 전달할 수 있다.
즉시 실행 함수 내에 코드를 모아 두면, 혹시 모를 변수/함수 이름의 충돌을 방지할 수 있다. 이것은 14장에서 자세히 알아볼 예정이다.

2. 재귀 함수

  • 재귀 호출(자기 자신을 호출)을 수행하는 함수
  • 반복 처리를 위해 사용됨
function factorial(n){
  if (n <= 1) return 1;
  return n * factorial(n-1);
}

// 풀이 과정
factorial(3) = 3 * factorial(2)
             = 3 * (2 * factorial(1))
             = 3 * (2 * 1)
             = 3 * 2
             = 6

factorial 함수에서 자기 자신을 호출할 때 사용한 식별자(factorial)는 함수 이름이다.
함수 이름은 함수 몸체 내부에서만 유효하다. 그렇기 때문에 함수 내부에서 함수 이름을 사용해 자기 자신을 호출할 수 있다.

재귀함수는 자신을 무한 재귀 호출하기 때문에 재귀 호출을 멈출 수 있는 탈출 조건이 필요하다.
재귀 함수는 반복문(for문, while문) 없이 반복되는 처리를 구현한다는 장점이 있다. 하지만 무한 반복에 빠질 위험으로 인한 스택 오버플로 에러 가능성이 있으므로 주의해서 사용한다.

일반적으로, 반복문보다 재귀 함수를 사용하는 편이 직관적으로 이해하기 쉬울 때만 사용하는 것이 좋다.

스택 오버플로 에러 프로그램의 실행 중에 호출 스택이 일정한 제한을 초과하여 메모리 스택이 가득 차서 발생하는 에러

3. 중첩 함수

  • 함수 내부에서 정의된 함수
  • 중첩 함수 또는 내부 함수
  • 외부 함수 : 중첩 함수를 포함한 함수
function outerFunction() {
  var x = 1;

  // 중첩 함수 정의
  function innerFunction() {
    var y = 2;
    console.log(x + y); // 외부 함수의 변수 참조 가능
  }

  // 중첩 함수 호출
  innerFunction();
}

// 외부 함수 호출
outerFunction();

ES6 이후 문이 위치할 수 있는 문맥이라면, 함수 정의가 어디든지 가능하다.
ES6 이전에는 함수 선언문의 경우 코드의 최상위, 다른 함수 내부에서만 정의가 가능했다.
ES6 이후 if문, for문 등의 코드 블록 내에서도 정의가 가능하다. 하지만 호이스팅으로 인한 혼란이 발생할 수 있으므로 그것은 바람직하지 않다.

중첩 함수는 스코프, 클로저와 깊은 관련이 있는데, 이에 대해선 뒤에서 언급할 예정이다.

중첩 함수는 주로 외부 함수의 스코프에서 변수를 공유하고, 클로저(closure)를 형성할 때 사용됨
클로저는 함수와 그 함수가 선언된 스코프에 대한 참조를 포함하며, 이를 통해 외부 함수의 변수에 접근할 수 있게 함

4. 콜백 함수

  • 함수의 매개변수를 통해 다른 함수의 내부로 전달되어,
    특정 이벤트가 발생하거나 특정 조건이 충족되었을 때 호출되는 함수
  • 고차 함수 : 매개변수를 통해 외부에서 콜백 함수를 전달받은 함수
function repeat1 (n) {
  for (var i=0; i<n; i++) console.log(i);
} 
function repeat2 (n) {
  for (var i=0; i<n; i++) {
    if (i % 2) console.log(i); // 홀수만
  }
}

repeat1(5); // 0 1 2 3 4
repeat2(5); // 1 3

여기 비슷한 기능을 수행하는 함수 2개가 있다. 공통으로 n번 반복하는 일을 실행하지만, 하는 일은 다르다.
함수의 일부분만 다르기 때문에 짝수만 console을 찍는 기능이 필요하다면, 또 새롭게 함수를 정의해야한다.

이때 콜백 함수를 사용하면, 함수의 변하지 않는 공통 로직을 미리 정의하고
경우에 따라 변경되는 로직을 함수 외부에서 함수 내부로 전달할 수 있다.

// 외부에서 전달받은 f를 n만큼 반복
function repeat (n, f) {
  for (var i=0; i<n; i++){
    f(i); // f 함수를 호출하며 i 인수 전달
  }
} 

var logAll = function (i) {
  console.log(i);
}
var logOdd = function (i) {
  if(i % 2) console.log(i);
}

// 반복 호출할 함수를 인수로 전달
repeat(5, logAll); // 0 1 2 3 4
repeat2(5, logOdd); // 1 3
  • repeat : 공통 로직, 고차 함수
  • logAll, logOdd : 변경 로직, 콜백 함수

일급 객체인 함수는 함수의 매개변수를 통해 함수를 전달할 수 있다.
repeat 함수는 외부에서 로직의 일부분을 함수로 전달받아 수행하여 더욱 유연한 구조를 갖게 되었다.
짝수만 로그를 찍는 기능을 추가하려면 변경 로직의 logEven 함수를 하나 더 만들면 된다.

콜백 함수는 고차 함수에 전달되면서 헬퍼 함수의 역할을 한다. 고정되어 교체하기 어려운 중첩 함수와 달리, 외부에서 고차 함수 내부로 주입되는 콜백 함수는 자유롭게 교체가 가능하다는 장점이 있다.
즉 고차 함수는 콜백 함수를 자신의 일부분으로 합성한다고 할 수 있다.

고차 함수는 콜백 함수의 호출 시점을 결정해서 호출한다.
즉, 콜백 함수는 고차 함수에 의해 호출되고, 이때 필요에 따라 고차 함수는 콜백 함수에 인수를 전달할 수 있다.
콜백 함수를 익명 함수 리터럴로 정의하면, 고차 함수가 호출될 때마다 콜백 함수가 생성된다.

콜백 함수 활용

  • 비동기 처리 (이벤트 처리, Ajax 통신, 타이머 함수 등)
  • 배열 고차 함수 (map, filter, reduce) 27장 참고

5. 순수 함수와 비순수 함수

  • 순수 함수 부수효과가 없는 함수 (어떤 외부 상태에 의존하지도 않고 변경하지도 않는 함수)
  • 비순수 함수 부수효과가 있는 함수 (어떤 외부 상태에 의존하거나 외부 상태를 변경하는 함수)
    ㄴ 외부 상태 : 전역 변수, 서버 데이터, 파일, console. DOM 등
// 순수 함수의 예시
function add(a, b) {
  return a + b;
}
const result = add(3, 5); // 항상 8을 반환, 부작용 없음

// 비순수 함수의 예시
let counter = 0;
function impureAdd(a) {
  counter++;
  return a + counter;
}
const result1 = impureAdd(3); // 결과는 4, 부작용: counter 값 변경
const result2 = impureAdd(3); // 결과는 6, 부작용: counter 값 변경

순수 함수

순수 함수는 동일한 인수가 전달되면 언제나 동일한 값을 반환한다.
즉, 어떤 외부 상태에도 의존하지 않고 오직 매개변수로 전달된 인수에만 의존하여 값을 생성해 반환한다.
내부 상태가 호출될 때마다 변화하는 값(예: 현재 시간)이라면 이것 역시 순수 함수가 아니다.

순수 함수는 일반적으로 최소 하나 이상의 인수를 전달받는다.
인수를 전달받지 않은 순수 함수는 언제나 동일한 값을 반환하므로 상수와 같아 의미가 없다.
또한 순수 함수는 인수를 변경하지 않는 것이 기본이다. 즉, 순수 함수는 인수의 불변성을 유지한다.

비순수 함수

비순수 함수는 함수의 외부 상태에 따라 반환값이 달라진다. 비순수 함수는 외부 상태를 변경하는 부수 효과가 있다.
만약, 인수를 전달받지 않고 함수 내부에서 외부 상태를 직접 참조하면, 상태 변화를 추적하기가 어려워진다.
코드의 복잡성을 증가시키는 비순수 함수를 최대한 줄여 부수 효과를 최대한 억제하는 것이 좋다.

함수형 프로그래밍은 외부 상태를 변경하는 부수 효과를 최소화하여 불변성을 지향하는 프로그래밍 패러다임이다. 자바스크립트는 멀티 패러다임 언어로, 객체지향 프로그래밍과 함수형 프로그래밍을 적극적으로 활용한다.


드디어 함수를 한번 훑어봤다. 멋사 포스팅과 병행하니 1주일은 걸린 것 같다...!
그래도 함수 선언문보단 함수 표현식을 더 사용해야 한다는 것을 새롭게 알았고, 괜히 어려워서 겁내던 콜백함수도 조금 친근해진 것 같다. 모던 자스에 딥다이브 하고 있는데 .. 나 다이빙 꽤 좋아하네 .. 잼있네 ,, ,, (코 쓱)

profile
" 퍼블리셔에서 프론트엔드로 Level up 중 ... 💨 "

0개의 댓글