JavaScript 18분 읽기

함수

이전에 자바스크립트의 거의 모든 것은 객체라고 했습니다. 함수 역시 '일급객체'입니다. 객체처럼 프로퍼티를 가질 수 있고, 값으로 변수에 할당될 수 있다는 의미죠.

함수

이전에 자바스크립트의 거의 모든 것은 객체라고 했습니다. 함수 역시 ‘일급객체’입니다. 객체처럼 프로퍼티를 가질 수 있고, 값으로 변수에 할당될 수 있다는 의미죠.

함수는 많이 들어보셨을 거라고 생각합니다. 수학문제를 풀 때 함수로 f(x) 같은 것을 봤을 겁니다. 그러면 그때로 돌아가서 함수가 무엇이었는지 떠올려 봅시다.

f(x) = x + 1 // x값에 1을 더하는 함수

위 식을 보면 뭐가 떠오르시나요? 저는 그래프가 생각납니다. 함수를 배울 때 그래프를 같이 배웠던 기억이 있으니까요. 하지만 프로그래밍에서는 그래프를 생각하지 않아도 됩니다. 위의 식을 보면, x값이 무엇이 들어오든 결과는 그 값에 1을 더한 값이 되겠죠. 프로그래밍에서 함수도 이와 비슷하게 동작합니다. 어찌보면 거의 동일하게 동작한다고 할 수 있겠네요.

프로그래밍에서 함수는 코드의 재사용 측면에서 아주 좋은 역할을 합니다. 반복되는 코드를 함수로 만들어 호출만 한다면 그 코드를 계속 사용할 수 있죠. 또한, 항상 같은 코드가 아닌 유동적으로 변하는 결과를 받을 수 있습니다. 재사용이 쉽다면 코드 유지보수도 용이하고 이로 인해 오류 발생 가능성도 적게 되겠죠.

함수는 입력되는 값이 있고, 이를 인자라고 합니다. 그 후 특정 코드를 실행하고 그 결과를 반환하게 되죠. 꼭 결과를 반환하지 않아도 됩니다. 특정 코드를 실행한다는 것이 중요합니다. 그 코드는 개발자인 우리가 작성할 것이고요. 우선 형태를 먼저 봅시다.

// 함수 선언문
function foo(매개변수){
  // 실행될 코드
}

// 함수 호출 표현식
foo(인자);

위와 같은 형태로 함수를 선언하고, 호출하여 사용합니다. 호출하는 것은 수학에서 f(x)라는 함수에 f(1)을 넣어 결과를 얻는 것과 같죠? 위의 수학식에서 결과는 2가 나올 겁니다. 그럼 이 함수를 자바스크립트로 작성해보겠습니다.

function f(num){
  return num + 1;
}

f(1); // 2

함수 f에 1을 넣으면 2가 결과로 반환됩니다. 눈치채신 분도 있겠죠? 결과를 반환할 때는 return이라는 키워드를 사용합니다. 하지만 꼭 반환값이 있을 필요는 없어요.

function sayHello(){
  console.log('hello'); // 'hello'
}
sayHello();

위의 코드를 실행하면 ‘hello’가 출력됩니다. 이런 식으로 반환하는 값이 없어도 함수는 잘 동작합니다. 하지만, 둘의 차이는 있겠죠?

위의 코드들은 함수를 정의하고 호출하는 모습을 보여드린 것입니다. 함수를 정의만 한다고 해서 함수 내부의 코드가 실행되는 것이 아니라, 함수를 호출해야 내부 코드가 실행된다는 것을 기억하세요.

함수 정의

함수를 선언하는 것을 정의한다고 표현합니다. 함수를 정의하는 방법은 여러 가지가 있는데요.

  • 함수 선언문
  • 함수 표현식
  • Function 생성자 함수
  • 화살표 함수

위의 4가지 경우를 생각하면 됩니다. 생성자 함수를 이용하는 경우는 거의 사용하지 않기 때문에 나머지를 보여드릴게요.

// 함수 선언문
function foo(){
  // 함수 내부 코드 
}

// 함수 표현식
var foo = function foo(){
  // 함수 내부 코드
}

// 화살표 함수
var foo = ()=>{
  // 함수 내부 코드
}

위와 같은 형태로 함수를 정의할 수 있습니다. 각 정의 방식은 모두 함수를 정의한다는 공통점이 있지만, 아주 약간 다른 부분도 있습니다. 호출하는 방법은 모두 같습니다.

함수 선언문

함수 선언문은 function키워드와 함수이름, 매개변수, 필요하다면 return키워드를 사용해서 표현합니다. 호출 시 코드 블록문 안의 내용이 실행되는 모습이죠.

// 함수 선언문
function foo(){
  // 함수 내부 코드 
}

함수 선언문은 표현식이 아닙니다. 그렇기 때문에 변수에 할당하는 것처럼 값으로 사용될 수 없죠. 사실, 이 부분이 조금 헷갈릴 수 있습니다. 위의 ‘함수 표현식’ 코드는 마치 변수에 함수 선언문을 할당하는 것처럼 보이기 때문이죠. 함수 표현식은 함수 선언문과 같은 형태일 수 있지만, 값으로써 사용되면 표현식이 됩니다.

같은 형태일 수 있다는 것은, 다른 형태도 있다는 것이겠죠? 바로 표현식에서는 함수명을 생략할 수 있습니다. 이를 ‘익명’함수라고합니다. 이름이 없는 것이죠. 함수 선언문에서는 이름을 생략할 수 없습니다. 함수 선언문은 표현식이 아니지만, 값으로써 쓰이면 표현식으로 인지할 수 있다는 것입니다.

함수 표현식

함수 표현식은 위에서 설명한 것처럼 표현식으로 쓰이기 때문에 변수에 할당하는 형태로 쓰이는 경우입니다.

var foo = function (){
  
}

이 함수는 익명함수로 표현식을 작성했네요. 함수를 호출할 때 할당한 변수를 이용하여 호출하면 됩니다.

함수 선언문 VS 함수 표현식

둘의 차이를 위에서 조금 설명했습니다. 선언문은 표현식이 아니다, 표현식은 익명함수로 변수에 할당될 수 있다 정도였지요. 그것 말고도 다른 차이가 있습니다. 아래의 코드를 확인해보세요.

foo1(); // 함수 foo1를 호출

console.log(foo1); // 함수 foo1을 출력
console.log(foo2); // 함수 foo2를 출력

function foo1(){
  console.log('foo1');
}
var foo2 = function(){
  console.log('foo2');
}

위 코드는 어떤 결과가 출력될까요? 첫 줄부터 함수 선언 없이 호출하니까 오류가 발생하지 않을까요? 또 함수 객체를 출력하는 2번째, 3번째 코드도 오류가 나야하는 것이 아닐까요?

이 코드를 실행하면 조금 이상합니다. foo1을 호출하면 ‘foo1’이 출력이 되고, foo1을 콘솔에 출력하면 foo1 함수 객체가 출력이 됩니다. 하지만 foo2는 그렇지 않죠. undefined가 출력됩니다. 예상과 전혀 다른 결과네요.

그 전에 이와 비슷한 경우가 있었는지 생각해보죠.

console.log(test); // undefined
var test = 1;

변수를 선언하기 전에 콘솔에 출력하면 undefined가 출력되는 것을 본 적이 있습니다. 바로 호이스팅이란 것이었죠. 자바스크립트에서 변수 선언문을 최상단으로 가져오는 것처럼 작동하고, undefined를 먼저 할당한다 뭐 이런 내용이었던 거 같네요.

이를 보았을 때 foo2undefined를 출력하는 것은 이해가 됩니다. 변수 foo2를 최상단으로 올린 후 undefined를 할당했다는 것이죠. 하지만, foo1은 조금 다릅니다. 바로 함수의 호이스팅이 된 것인데요.

함수는 호이스팅 될 때, undefined를 할당하는 것이 아니라 함수 객체 자체를 할당해서 선언문이 나오기 전에 호출이 가능한 것이죠. 즉, 변수처럼 함수 선언문이 최상단으로 올려져 평가되고, 함수 객체를 할당하여 런타임 시에는 함수가 이미 존재하게 되는 것입니다.

이렇게 함수 호이스팅이 발생하기 때문에 함수 선언문보다는 표현식을 이용하는 것이 더 좋은 방법일 수 있습니다.

화살표 함수

ES6부터 화살표 함수를 지원합니다. 화살표 함수는 this와 관련된 문제를 해결할 수 있는 수단이 될 수 있습니다. 일반 함수와 다르게 익명함수로 작성합니다. 그 외의 구체적인 차이점은 추후 포스팅하겠습니다.

var foo = ()=>{
  
}

함수 호출

함수를 호출하는 것은 이전에 보여드렸듯이 아래와 같습니다.

var foo = function (){

}

foo();

함수명에 괄호를 붙여주는 것이죠.

함수를 구성하는 요소에는 매개변수, 인자, 반환값이 있습니다. 매개변수는 함수 정의 시에 인자로 받을 변수입니다. 인자는 함수 호출 시에 함수에 넘겨주게 되는 값입니다. 반환값은 함수가 종료될 때 함수 외부로 전해주는 결과라고 생각하면 됩니다. 다시 처음 예시로 돌아가서

var f = function (x){
  return x + 1;
}
f(1);

위의 코드에서 함수 f는 인자를 x라는 매개변수로 받을 것이고, 결과로 x + 1을 반환합니다. 또한, 호출 시 인자로 1을 넘겨줍니다. 이것이 함수 호출의 기본 형태라고 생각하면 됩니다.

인자는 함수 내부에서 매개변수에 할당되어 일종의 변수로 사용됩니다. 반환값은 함수 종료 시 함수 호출식의 값으로 역할합니다. 즉 f(1)이 결과값으로 변한다고 생각하면 됩니다. 그렇다는 것은 그 값을 또 사용할 수 있다는 것이겠죠.

var f = function (x){
  return x + 1;
}

var foo = f(1); // foo에 2가 할당됩니다.

위의 코드처럼 함수를 호출하면 반환값이 그 자리를 채웁니다. 만약, 반환값이 없다면 어떻게 될까요?

function foo(){
  console.log('foo');
}
console.log(foo());

// 'foo'
// undifined

위의 함수는 반환값이 없습니다. 하지만 내부에서 콘솔에 ‘foo’를 출력하네요. 그 후 외부에서 콘솔에 foo함수를 호출해 출력하는 코드가 있습니다. 그렇다면 foo함수의 반환값이 출력되야 하지만, 반환값이 없으므로 undefined가 출력되는 것을 확인할 수 있습니다. 즉, 반환값이 없다면 함수는 undefined를 반환한다는 것을 알 수 있죠.

함수의 호출은 함수의 이름으로 하지 않습니다. 함수를 가리키는 식별자로 호출합니다.

	var foo = function test(){
      console.log('test');
    }
    foo(); // 함수를 할당한 식별자인 'foo'로 호출합니다.

함수 선언문의 경우 자바스크립트 엔진이 자동으로 함수 이름과 같은 식별자를 붙여주게 됩니다.

매개변수와 인자

함수의 매개변수는 여러 개를 사용할 수 있습니다. 하지만, 많은 매개변수를 받는 것은 그렇게 좋은 코드라고 할 수 없죠. 차라리 매개변수로 객체를 받고 객체의 프로퍼티로 함수를 동작하는 것이 더 좋을 수 있습니다.

그렇다면 함수의 매개변수보다 적은 수의 인자로 함수를 호출하게 되었을 때는 어떻게 될까요?

function foo(a,b,c){
  console.log(a,b,c);
}
foo(1); // 1 undefined undefined

위와 같이 에러없이 호출이 되지만 값이 없는 매개변수는 undefined가 할당됩니다. 이런 경우 초기값을 설정해주는 것도 방법입니다.

function foo(a=0,b=0,c=0){
  console.log(a,b,c);
}
foo(1); // 1 0 0

이렇게 초기화하게 되면 인자를 넘겨받지 못한 매개변수의 값을 설정할 수 있습니다.

그렇다면 매개변수보다 넘겨준 인자가 많을 때는 어떻게 될까요?

function foo(a,b){
  console.log(a,b);
  
}
foo(1,2,3); // 1 2

에러없이 작동하게 됩니다. 순서대로 매개 변수에 할당이 되고 나머지는 무시됩니다. 사실, 엄밀히 말하면 초과된 인자는 arguments 객체로 접근할 수 있습니다. 함수에 인자를 넣게되면 arguments객체에 배열의 원소처럼 저장되게 됩니다. 이 객체도 함수 내부에서만 사용할 수 있습니다.

function foo(a,b){
  console.log(a,b);
  console.log(arguments[2]);
}
foo(1,2,3);
// 1 2
// 3

위 코드에서는 초과된 인자를 arguments객체에서 불러오고 있습니다.

그렇다면 만약 넘겨 받은 인자를 바꾸게 되면 어떻게 될까요?

function add(a){
  a = 2
  return a + 1;
}
var t = 1;
add(t);
console.log(t); // 1

변수로 넘겨준 값은 함수 내부에서 연산을 하더라도 변하지 않습니다. 만약 변한다고 했다면, t는 1이 아니라 3이 출력되었어야 됩니다. 바로 변경 불가능한 값이기 때문입니다.

function change(obj){
  obj.age = 19;
}
var person = { name : 'kim' };
change(person);
console.log(person); // { name: 'kim, age: 19 }

하지만 객체는 다릅니다. 바로 객체는 변경 가능한 값이기 때문이죠. 인자로 객체를 넘기면 객체가 넘어가는 것이 아니라 객체가 있는 주소가 넘어가게 되죠. 그 후 함수에서 매개변수로 그 주소에 접근하면 실제 객체의 접근이 되는 것입니다.

함수 외부의 값을 변경하는 것은 최대한 주의하세요. 예상치 못한 오류가 발생할 수 있습니다. 코드의 안정성이 낮아지는 것이죠.

원본객체의 변경없이 함수에서 바꾸고 싶다면, 깊은 복사를 이용해야 합니다. 원시 타입에서 복사를 하는 것처럼 객체를 완전히 동일한 형태로 새로운 방에 생성하는 것이죠. 하지만 생각처럼 깊은 복사가 단순하지는 않습니다. 객체 내부에 프로퍼티 값으로 객체를 할당할 수 있기 때문이죠. 외부 객체를 복사했다고 해도 내부 객체를 복사하지 않으면 내부 객체는 아직 공유되고 있는 상태일 겁니다.

함수 내부 코드

함수의 내부에 코드를 적게 되면 함수 호출 시 그 코드가 실행된다고 했습니다. 그렇다면, 함수 내부에서 선언한 변수들은 어떻게 될까요?

var x = 11;
function foo(){
    var x = 10;
	console.log(x); // 10
}
foo();
x // 11

위와 같이 함수 내부에서 선언한 변수는 함수 내부에서만 생성되고 사용되며, 함수가 종료되면 사라집니다. 함수의 매개변수도 마찬가지로 함수 내부에서만 유효합니다. 외부에서 접근할 수 없죠.

이렇게 변수가 유효한 범위를 스코프라고 합니다. 스코프에 대해서는 추후 자세하게 포스팅해보겠습니다.

여러 함수의 형태

즉시 실행 함수

함수는 코드를 재사용할 때 좋다고 했습니다. 그렇다면, 즉시 실행 함수는 왜 필요할까요?

위에서 봤듯이, 함수 내부에 변수를 선언하게 되면, 함수의 외부에서 그 변수에 접근할 수 없게 됩니다. 이것을 지역변수라고 합니다. 반면, 어디서든 접근이 가능한 것을 전역변수라고 할 수 있죠. 전역변수가 많아지면 전역 스코프를 오염시킬 수 있습니다. 이러한 문제들 때문에 즉시 실행 함수를 사용 할 수 있죠.

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

즉시 실행 함수는 말그대로 함수 선언과 동시에 실행되는 형태입니다. 표현 방법은 여러가지가 있지만, 저는 위와 같은 표현을 사용합니다.

즉시 실행 함수는 바로 실행되기 때문에 바로 사라진다고도 할 수 있습니다. 그래서 굳이 이름을 붙일 필요가 없겠죠.

재귀 함수

재귀함수는 함수 내부에서 자기 자신을 또 호출하는 함수를 말합니다.

// 팩토리얼을 구하는 문제를 재귀함수를 이용해서 풀었습니다.
function factorial(num){
  if(num === 1) return 1;
  else{
    return num * factorial(num-1);
  }
}

위의 코드는 팩토리얼을 재귀함수를 이용해 작성한 것입니다. 함수 내부에서 자기 자신을 호출한다는 것을 확인할 수 있죠. 단, 함수가 계속 자신을 호출하지 않도록 탈출 조건을 반드시 만들어야 합니다. 위 코드의 경우 num === 1이 탈출 조건이 되는 것이죠.

재귀함수가 탈출하지 못하게 되면 콜스택에 함수가 계속 쌓이게 되어 스택 크기를 넘어가는 스택 오버플로우가 발생하게 됩니다.

중첩 함수

중첩함수는 함수 내부에서 함수를 정의하는 것입니다.

function outer(){
  console.log('outer');
  function inner(){
    console.log('inner');
  }
  inner();
}
outer();
// 'outer'
// 'inner'

내부 함수는 바로 외부 함수에서만 호출 가능합니다. 외부 함수의 밖에서는 호출할 수 없죠. 지역 변수와 비슷하지 않나요?

콜백 함수

자바스크립트의 함수는 객체라고 했습니다. 마치 객체를 인자로 넘겨주듯이 함수 역시 인자로 넘겨줄 수 있습니다. 이때 인자로 넘겨진 함수를 콜백함수라고 합니다.

또한, 인자로 콜백함수를 받는 함수를 고차 함수라고 합니다. 고차함수에서는 콜백함수의 호출 시기를 결정해 호출합니다.

function foo(a, f){
  console.log(a);
  f();
}
foo('HOF',function(){
  console.log('callback function');
})

// HOC
// callback function

위의 코드와 같이 함수 호출 시 함수를 인자로 넘겨줄 수 있습니다. 이때 콜백함수는 꼭 호출 시 선언 될 필요는 없습니다. 오히려 호출 시마다 함수를 생성해서 비효율적입니다. 차라리 함수를 미리 선언한 후 그 함수를 인자로 넘겨주는 것이 효율적이죠.

// 위의 코드에서 선언만 했습니다.
function foo(a, f){
  console.log(a);
  f();
}

function callback(){
  console.log('callback function');
}

foo('HOF',callback);

이때 함수명을 넣어 함수 객체를 보낸다는 것을 주의하세요. callback 함수를 인자로 넘겨줄 때는 호출하는 것이 아니라 함수 객체 자체를 보내는 것입니다. 만약 호출을 해서 보낸다면, 함수의 반환값을 보내게 되겠죠.