자바스크립트 함수지향

0. 함수지향

이번에는 자바스크립트의 함수지향 부분을 정리해보려고 합니다.

자바스크립트의 핵심적인 도구는 함수입니다. 자바스크립트의 함수는 매우 강력합니다. 함수에 대한 이해 없이는 자바스크립트를 잘 다루기 어렵다고 합니다. 또한 자바스크립트에서 함수는 객체를 이해하는데 가장 중요한 기초를 이룹니다.

1. 유효범위

유효범위(Scope)는 변수의 수명을 의미합니다.

const vscope = 'global'
const fscope = () => {
  console.log(vscope)
}
fscope()

// result : global

함수 밖에서 변수를 선언하면 그 변수는 전역변수가 됩니다. 전역변수는 어플리케이션 전역에서 접근이 가능한 변수입니다. 어떤 함수에서도 그 변수에 접근이 가능합니다.

const vscope = 'global'
const fscope = () => {
  const vscope = 'local'
  console.log('함수안 ' + vscope)
}
fscope()
console.log('함수밖 ' + vscope)

/*
함수안 local
함수밖 global
*/

함수 내에서 선언한 지역변수와 함수 밖에서 선언된 전역변수입니다. 지역변수의 유효범위는 함수 안이고, 전역변수의 유효범위는 어플리케이션 전역입니다. 변수가 동시에 정의된다면 지역변수가 우선한다는 것을 알 수 있습니다.

전역변수는 사용하지 않는 것이 좋습니다. 여러가지 이유로 그 값이 변경될 수 있기 때문입니다. 만약 함수 안에서 전역변수를 사용하고 있다면 누군가에 의해서 전역변수의 값이 달라진다면 함수의 동작도 달라지게 됩니다. 또한 함수를 다른 어플리케이션에 이식하는데 어려움을 초래합니다.

전역변수를 사용해야 하는 경우라면 그것을 사용하는 이유를 명확히 알고 사용해야 합니다.

1-1. 전역변수의 사용

불가피하게 전역변수를 사용해야 하는 경우는 하나의 객체를 전역변수로 만들고 객체의 속성으로 변수를 관리하는 방법을 사용합니다.

const MYAPP = {}
MYAPP.calculator = {
  left: null,
  right: null,
}
MYAPP.coordinate = {
  left: null,
  right: null,
}

MYAPP.calculator.left = 10
MYAPP.calculator.right = 20
const sum = () => MYAPP.calculator.left + MYAPP.calculator.right

console.log(sum())
// result : 30

만약 전역변수를 사용하고 싶지 않다면 익명함수를 호출함으로서 가능합니다.

;(function() {
  const MYAPP = {}
  MYAPP.calculator = {
    left: null,
    right: null,
  }
  MYAPP.coordinate = {
    left: null,
    right: null,
  }
  MYAPP.calculator.left = 10
  MYAPP.calculator.right = 20
  const sum = () => MYAPP.calculator.left + MYAPP.calculator.right
  console.log(sum())
})()
// result : 30

위와 같은 방법은 자바스크립트에서 로직을 모듈화하는 일반적인 방법입니다.

1-2. 유효범위의 대상 (함수)

자바스크립트는 함수에 대한 유효범위를 제공합니다. 많은 언어들이 블록(대체로 {})에 대한 유효범위를 제공하는 것과 다른점입니다. (물론 let, const로 변수를 지정하면 변수는 블록에 대한 유효범위를 가져갑니다.)

for (let i = 0; i < 1; i++) {
  var name = 'coding everybody'
}
console.log(name) // result : coding everybody
for (let i = 0; i < 1; i++) {
  const name = 'coding everybody'
}
console.log(name)

밑에 const로 지정된 변수는 블록 밖에서는 불릴 수 없습니다.

1-3. 정적 유효범위

자바스크립트는 함수가 선언된 지점에서의 유효범위를 가집니다. 이러한 유효범위의 방식을 정적 유효범위(static scoping), 혹은 렉시컬(lexical scoping)이라고 합니다.

const i = 5

function a() {
  const i = 10
  b()
}

function b() {
  console.log(i)
}

a()
// result : 5

2. 값으로서의 함수와 콜백

2-1. 값으로서의 함수

자바스크립트에서 함수도 객체입니다. 다시 말해서 일종의 값입니다. 거의 모든 언어가 함수를 가지고 있습니다. 자바스크립트의 함수가 다른 언어의 함수와 다른 점은 함수가 값이 될 수 있다는 점입니다.

function a() {}

위의 예제에서 함수 a는 변수 a에 담겨진 값입니다. 또한 함수는 객체의 값으로 포함될 수 있습니다. 이렇게 객체의 속성 값으로 담겨진 함수를 메소드(method)라고 부릅니다.

a = {
  b: function() {},
}

함수는 값이기 때문에 다른 함수의 인자로 전달 될수도 있습니다.

const cal = (func, num) => func(num)
const increase = num => num + 1
const decrease = num => num - 1
console.log(cal(increase, 1)) // result : 2
console.log(cal(decrease, 1)) // result : 0

cal(increase, 1)에서 함수 increase와 값 1이 함수 cal의 인자로 전달됩니다. 함수 cal은 첫번째 인자로 전달된 increase를 실행하는데 이 때 두번째 인자의 값이 1을 인자로 전달합니다. 함수 increase은 계산된 결과를 리턴하고 cal은 다시 그 값을 리턴합니다.

함수는 함수의 리턴 값으로도 사용할 수 있습니다.

const cal = mode => {
  const funcs = {
    plus: (left, right) => left + right,
    minus: (left, right) => left - right,
  }
  return funcs[mode]
}
console.log(cal('plus')(2, 1)) // result : 3
console.log(cal('minus')(2, 1)) // result : 1

배열의 값으로도 사용할 수 있습니다.

const process = [
  input => input + 10,
  input => input * input,
  input => input / 2,
]
let input = 1
for (let i = 0; i < process.length; i++) {
  input = process[i](input)
}
console.log(input) // result : 60.5

2-2. 콜백

2-2-1. 처리의 위임

값으로 사용될 수 있는 특성을 이용하면 함수의 인자로 함수로 전달할 수 있습니다. 값으로 전달된 함수는 호출될 수 있기 때문에 이를 이용하면 함수의 동작을 완전히 바꿀 수 있습니다.

const sortNumber = (a, b) => b - a
const numbers = [20, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
console.log(numbers.sort(sortNumber)) // result : [20, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]

인자로 전달된 함수 sortNumber의 구현에 따라서 sort의 동작방법이 완전히 바꿔지게 됩니다.

2-2-2. 비동기 처리

콜백은 비동기처리에서도 유용하게 사용됩니다. 시간이 오래걸리는 작업이 있을 때 이 작업이 완료된 후에 처리해야 할일을 콜백으로 지정하면 해당 작업이 끝났을 때 미리 등록한 작업을 실행하도록 할 수 있습니다.

3. 클로저

클로저(closure)는 내부함수가 외부함수의 맥락(context)에 접근할 수 있는 것을 가르킵니다. 클로저는 자바스크립트를 이용한 고난이도의 테크닉을 구사하는데 필수적인 개념을 활용 될 수 있습니다.

3-1. 내부함수

자바스크립트는 함수 안에서 또 다른 함수를 선언할 수 있습니다.

const outter = () => {
  const inner = () => {
    const title = 'coding everybody'
    console.log(title)
  }
  inner()
}
outter()
// result : coding everybody

위의 예제에서 함수 outter의 내부에는 함수 inner가 정의 되어 있습니다. 함수 inner를 내부 함수라고 합니다.

내부함수는 외부함수의 지역변수에 접근 할 수 있습니다.

const outter = () => {
  const title = 'coding everybody'
  const inner = () => console.log(title)
  inner()
}
outter()
// result : coding everybody

위의 예제는 내부함수 inner에서 title을 호출했을때 외부함수인 outter의 지역변수에 접근할 수 있음을 보여줍니다.

3-2. 클로저

클로저(closure)는 내부함수와 밀접한 주제를 가지는 있는 주제입니다. 내부함수는 외부함수의 지역변수에 접근할 수 있는데 외부함수의 실행이 끝나서 외부함수가 소멸된 이후에도 내부함수가 외부함수의 접근에 접근 할 수 있습니다. 이러한 메커니즘을 클로저라고 합니다.

const outter = () => {
  const title = 'coding everybody'
  return () => console.log(title)
}
inner = outter()
inner()
// result : coding everybody

inner = outter()에서 함수 outter를 호출하고 있습니다. 그 결과가 변수 inner에 담기게 됩니다. 이 결과는 이름이 없는 함수입니다.

다음 행에서 outter 함수는 실행이 끝났기 때문에 이 함수의 지역변수는 소멸되는 것이 자연스럽습니다. 하지만 이 행에서 함수 inner를 실행했을 때 coding everybody가 출력된 것은 외부함수의 지역변수 title이 소멸되지 않았다는 것을 의미합니다.

클로저란 내부함수가 외부함수의 지역변수에 접근 할 수 있고, 외부함수는 외부함수의 지역변수가 소멸될 때까지 소멸되지 않는 특성을 의미합니다.

function factory_movie(title) {
  return {
    get_title: function() {
      return title
    },
    set_title: function(_title) {
      title = _title
    },
  }
}
ghost = factory_movie('Ghost in the shell')
matrix = factory_movie('Matrix')

console.log(ghost.get_title())
console.log(matrix.get_title())

ghost.set_title('공각기동대')

console.log(ghost.get_title())
console.log(matrix.get_title())
/*
result :
Ghost in the shell
Matrix
공각기동대
Matrix
*/
  1. 클로저는 객체의 메소드에서도 사용할 수 있습니다. 위의 예제는 함수의 리턴값으로 객체를 반환하고 있습니다. 이 객체는 메소드 gettitle, settitle를 가지고 있습니다. 이 메소드들은 factory_movie의 인자값으로 전달된 지역변수 title을 사용할 수 있습니다.
  2. 동일한 외부함수 안에서 만들어진 내부함수나 메소드는 외부함수의 지역변수를 공유합니다. 먼저 실행된 settitle은 외부함수 factorymovie의 지역변수 title의 값을 ‘공각기동대’로 변경했습니다. 마지막에서 두번째로 실행되는 ghost.gettitle()의 값이 ‘공각기동대’인 것은 settitle와 get_title 함수가 title의 값을 공유하고 있다는 의미입니다.
  3. 그런데 똑같은 외부함수 factorymovie를 공유하고 있는 ghost와 matrix의 getttile의 결과는 서로 각각 다릅니다. 그것은 외부함수가 실행될 때마다 새로운 지역변수를 포함하는 클로저가 생성되기 때문에 ghost와 matrix는 서로 완전히 독립된 객체가 됩니다.
  4. factorymovie의 지역변수 title은 정의된 객체의 메소드에서만 접근할 수 있는 값입니다. title의 값을 읽고 수정할 수 있는 것은 factorymovie 메소드를 통해서 만들어진 객체 뿐이라는 의미입니다. 자바스크립트는 기본적으로 Private한 속성을 지원하지 않는데, 클로저의 이러한 특성을 이용해서 Private한 속성을 사용할 수 있게 됩니다.

참고 Private 속성은 객체의 외부에서는 접근 할 수 없는 외부에 감춰진 속성이나 메소드를 의미합니다. 이를 통해서 객체의 내부에서만 사용해야 하는 값이 노출됨으로서 생길 수 있는 오류를 줄일 수 있습니다. 자바와 같은 언어에서는 이러한 특성을 언어 문법 차원에서 지원하고 있습니다.

4. arguments

함수에는 arguments라는 변수에 담긴 숨겨진 유사 배열이 있습니다. 이 배열에는 함수를 호출할 때 입력한 인자가 담겨 있습니다.

function sum() {
  let i,
    _sum = 0
  for (i = 0; i < arguments.length; i++) {
    console.log(`${i} : ${arguments[i]}`)
    _sum += arguments[i]
  }
  return _sum
}
console.log(`result : ${sum(1, 2, 3, 4)}`)
/*
result :
0 : 1
1 : 2
2 : 3
3 : 4
result : 10
*/

함수 sum은 인자로 전달된 값을 모두 더해서 리턴하는 함수입니다. 함수의 정의부분에서 인자에 대한 구현이 없음에도 인자를 전달할 수 있는 이유는 arguments라는 특수한 배열이 있기 때문입니다.

arguments는 함수안에서 사용할 수 있도록 그 이름이나 특성이 약속되어 있는 일종의 배열입니다. arguments[0]은 함수로 전달된 첫번째 인자를 알아낼 수 있습니다. arguments.length를 이용해서 함수로 전달된 인자의 개수를 알아낼 수 있습니다. 이러한 특성에 반복문을 결합하면 함수로 전달된 인자의 값을 순차적으로 가져올 수 있습니다. 그 값을 더해서 리턴하면 인자로 전달된 값에 대한 총합을 구하는 함수가 만들 수 있습니다.

arguments은 사실은 배열이 아니며 실제로는 arguments 객체의 인스턴스입니다.

4-1. 매개변수의 수

매겨변수와 관련된 두가지 수가 있습니다. 하나는 함수.length, 다른 하나는 arguments.length 입니다.

arguments.length는 함수로 전달된 실제의 인수를 의미하고, 함수.length는 함수에 정의된 인자의 수를 의미합니다.

function zero() {
  console.log('zero.length', zero.length, 'arguments', arguments.length)
}
function one(arg1) {
  console.log('one.length', one.length, 'arguments', arguments.length)
}
function two(arg1, arg2) {
  console.log('two.length', two.length, 'arguments', arguments.length)
}
zero()
one('val1', 'val2')
two('val1')

/*
result :
zero.length 0 arguments 0
one.length 1 arguments 2
two.length 2 arguments 1
*/

5. 함수의 호출

function func() {}
func()

위는 함수를 호출하는 가장 기본적인 방법입니다.

자바스크립트는 함수를 호출하는 특별한 방법을 제공합니다. 자바스크립트에서 함수는 객체입니다. 위의 예제에서 함수 func는 Function이라는 객체의 인스턴스입니다. func는 객체 Function이 가지고 있는 메소드들을 상속하고 있습니다.

function sum(arg1, arg2) {
  return arg1 + arg2
}
console.log(sum.apply(null, [1, 2]))
// result : 3

함수는 sum은 Function 객체의 인스턴스입니다. 그렇기 때문에 객체 Function 의 메소드 apply를 호출 할 수 있습니다. apply 메소드는 두 개의 인자를 가질 수 있습니다. 첫 번째 인자는 함수가 실행될 맥락입니다. 두 번째 인자는 배열입니다. 이 배열의 담겨있는 원소가 함수의 인자로 순차적으로 대입됩니다.

o1 = { val1: 1, val2: 2, val3: 3 }
o2 = { v1: 10, v2: 50, v3: 100, v4: 25 }
function sum() {
  let _sum = 0
  for (name in this) {
    _sum += this[name]
  }
  return _sum
}
console.log(sum.apply(o1)) // result : 6
console.log(sum.apply(o2)) // result : 185

먼저 두개의 객체를 만들었습니다. o1는 3개의 속성을 가지고 있습니다. 각각의 이름은 val1, val2, val3 입니다. o2는 4개의 속성을 가지고 있고 o1과는 다른 속성 이름을 가지고 있고 속성의 수도 다릅니다.

그다음엔 함수 sum을 만들었습니다. 이 함수는 객체의 속성을 열거할 때 사용하는 for in문을 이용해서 객체 자신(this)의 값을 열거한 후에 각 속성 값을 지역 변수 _sum에 저장한 후에 이를 리턴한 후에 이를 리턴하고 있습니다.

객체 Function의 메소드 apply의 첫번쩨 인자는 함수가 실행될 맥락입니다. sum.apply(o1)은 함수 sum을 객체 o1의 메소드를 만들고 sum을 호출한 후에 sum을 삭제합니다.

sum의 o1 소속의 메소드가 된다는 것은 이렇게 바꿔 말할 수 있습니다. 함수 sum에서의 this의 값이 전역객체가 아니라 o1이 된다는 의미입니다. 일반적인 객체지향 언어에서는 하나의 객체에 소속된 함수는 그 객체의 소유물이 됩니다. 하지만 자바스크립트에서 함수는 독립적인 객체로서 존재하고, apply나 call 메소드를 통해서 다른 객체의 소유물인 것처럼 실행할 수 있습니다.

만약 apply의 첫 번째 인자로 null을 전달하면 apply가 실행된 함수 인스턴스는 전역객체(브라우저에서는 window)를 맥락으로 실행되게 합니다.


Written by@seongjoojin
Lives and works in Seoul building useful things.

GitHubTwitter