4.2 함수
// 함수의 기본적인 형태
function sum (a, b) {
return a + b
}
sum(10, 24) // 34
// 리액트에서 컴포넌트를 만드는 함수도 기초적인 함수 형태를 따른다.
function Component (props) {
return <div>{props.hello}</div>
}
Component
라고 하는 함수를 선언하고 매개변수로는 일반적으로 props
라고 부르는 단일 객체를 받으며 return
문으로 JSX
를 반환한다.
일반적인 함수와의 차이점이라고 한다면, 자바스크립트에서는 Component(props)
형태로 호출하지만, 리액트에서의 함수형 컴포넌트는 <Component hello={props.hello} />
와 같이 JSX 문법으로 단일 props
별로 받거나, <Component {...props} />
같은 형태로 모든 props를 전개 연산자로 받는다는 차이가 있다는 것이다.
그리고 이러한 JSX 형태 외에도 일반적인 자바스크립트 문법으로 함수형 컴포넌트를 호출하는 것도 가능하다.
4.2.1 함수를 정의하는 4가지 방법
4.2.1.1 함수 선언문
function add (a, b) {
return a + b
}
함수 선언문은 표현식이 아닌 일반 문(statement)으로 분류된다.
표현식이란 무언가 값을 산출하는 구문을 의미한다.
즉, 앞선 함수 선언으로는 어떠한 값도 표현되지 않았으므로 표현식이 아닌 문으로 분류된다.
그러나 다음 예제를 살펴보자.
const sum = function sum (a, b) {
return a + b
}
sum(10, 24) // 34
앞서 함수 선언문의 정의에 다르면, 함수 선언문은 말 그대로 '선언'이고 어떠한 값도 표현하지 않으므로 표현식과는 다르게 변수에 할당할 수 없는 것이 자연스러워 보인다.
그러나 위 예제는 마치 sum이라는 변수에 함수 sum을 할당하는, 표현식과 같은 작동을 보였다.
그 이유는 무엇일까?
이는 자바스크립트 엔진이 코드의 문맥에 따라 동일한 함수를 문이 아닌 표현식으로 해석하는 경우가 있기 때문이다.
따라서 위와 같이 이름을 가진 형태의 함수 리터럴은 코드 문맥에 따라 전자와 같은 선언문으로도, 후자와 같은 표현식으로도 사용될 수 있음을 알고 있어야 한다.
4.2.1.2 함수 표현식
함수 표현식에 대해 알아보기 전에 '일급 객체'라는 개념을 알고 있어야 한다.
프로그래밍 세계에서 일급 객체란 다른 객체들에 일반적으로 적용 가능한 연산을 모두 지원하는 객체를 의미한다.
자바스크립트에서 함수는 일급 객체다.
함수는 다른 함수의 매개변수가 될 수도 있고, 반환값이 될 수도 있으며, 앞에서 본 것처럼 할당도 가능하므로 일급 객체가 되기 위한 조건을 모두 갖추고 있다.
앞서 함수가 일급 객체라고 했으니, 함수를 변수에 할당하는 것은 당연히 가능하다.
const sum = function (a, b) {
return a + b
}
sum(10, 24) // 34
함수 표현식에서는 할당하려는 함수의 이름을 생략하는 것이 일반적이다.
그 이유는 코드를 봤을 때 혼란을 방지하기 위함이다.
const sum = function add(a, b) {
// 함수 몸통에서 현재 실행 중인 함수를 참조하는 데 사용할 수 있다.
// 이는 단순히 코드에 대한 이해를 돕기 위한 예제 코드고,
// 실제 프로덕션 코드에서는 절대로 사용해서는 안된다.
// https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Functions/arguments/callee
console.log(arguments.callee.name)
console.log(add)
return a + b;
}
sum(10, 24)
// add
// f add (a, b) { ...
// 34
add(10, 24) // Uncaught TypeError: add is not defined
위 함수 표현식 예제를 살펴보면 실제로 함수를 호출하기 위해서 사용된 것은 sum
임을 알 수 있다.
그리고 add
는 실제 함수 내부에서만 유효한 식별자일 뿐, 함수를 외부에서 호출하는 데에는 사용할 수 없는 식별자다.
따라서 함수 표현식에서 함수에 이름을 주는 것은 함수 호출에 도움이 전혀 안되는, 코드를 읽는 데 방해가 될 수 있는 요소임을 알 수 있다.
4.2.1.3 함수 표현식과 선언식의 차이
이 2가지 방식의 가장 큰 차이는 호이스팅(hoisting) 여부다.
먼저 호이스팅에 대해 알아보자.
함수의 호이스팅이라 함은, 함수 선언문이 마치 코드 맨 앞단에 작성된 것처럼 작동하는 자바스크립트의 특징을 의미한다.
다음 예제를 살펴보자.
hello() // hello
function hello () {
console.log('hello')
}
hello() // hello
함수를 선언한 hello는 코드 중간에 있음에도 불구하고, 맨 앞에서 호출한 hello()
는 어떠한 에러도 없이, 그리고 마치 함수가 미리 만들어지기라도 한 것처럼 정상적인 hello 함수의 작동을 수행하는 것을 알 수 있다.
함수의 호이스팅은 함수에 대한 선언을 실행 전에 미리 메모리에 등록하는 작업을 의미한다.
이러한 함수의 호이스팅이라는 특징 덕분에 함수 선언문이 미리 메모리에 등록됐고, 코드의 순서에 상관없이 정상적으로 함수를 호출할 수 있게 된 것이다.
반면 함수 표현식은 함수를 변수에 할당했다.
변수도 마찬가지로 호이스팅이 발생한다.
그러나 함수의 호이스팅과는 다르게, 호이스팅되는 시점에서 var
의 경우에는 undefined
로 초기화한다는 차이가 있다.
console.log(typeof hello === 'undefined') // true
hello() // Uncaught TypeError: hello is not a function
var hello = function () {
console.log('hello')
}
hello()
위 예제 코드는 앞선 함수 선언문과 다르게 정상적으로 호출되지 않고, undefined
로 남아있는 것을 알 수 있다.
함수와 다르게 변수는, 런타임 이전에 undefined
로 초기화되고, 할당문이 실행되는 시점, 즉 런타임 시점에 함수가 할당되어 작동한다는 것을 알 수 있다.
그렇다면 둘 중에 어떤 것이 좋을까?
함수를 자유롭게 선언하고 어디서든 자유롭게 호출하고 싶거나, 변수 선언과 다르게 명시적으로 함수를 구별하고 싶을 때는 함수 선언문이 더 좋을 수 있다.
함수 선언문은 함수가 선언된 위치에 상관없이 함수 호이스팅의 특징을 살리면 어디서든 호출할 수 있고, 또 변수 선언과 뚜렷하게 구별되는 차이점이 있다.
그러나 함수가 선언되기 전에 함수가 호출되는 것이 이상하게 느껴지는 사람도 있을 것이다.
함수 호출은 제일 먼저 보이고, 그 다음에 실제 함수를 어디서 어떻게 선언했는지는 해당 스코프를 끝까지 확인하지 않으면 개발자가 찾기 어렵다.
이는 관리해야 하는 스코프가 길어질 경우 특히 더 나쁘게 작용할 수 있다.
그리고 다른 언어를 주로 사용하던 개발자라면 이러한 모습이 어색하게 보일 수도 있다.
둘 중에 어떠한 것이 더 낫다거나 기능적으로 우위에 있다고 구별지을 만한 점은 없다.
현재 자바스크리븥 코드를 작성하는 환경을 살펴보고, 본인이나 프로젝트의 상황에 맞는 작성법을 일관되게 사용하면 충분하다.
4.2.1.4 Function 생성자
이 방법은 자바스크립트를 오래 사용했다 하더라도 거의 사용해본 적이 없을 만한 함수 선언 방식이다.
바로 Function
생성자를 활용하는 방법이다.
const add = new Function('a', 'b', 'return a + b')
add(10, 24) // 34
Function
생성자 함수를 사용해서 만든 모습은 썩 좋아보이지 않는다.
코드 작성 관점에서만 보더라도 매개변수, 그리고 함수의 몸통을 모두 문자열로 작성해야 한다.
이는 메모장에서 코드를 작성하는 것만큼이나 어려운 방법이며, 코드의 양이 길어진다면 더욱 혼란스러워질 것이다.
그리고 이렇게 생성자 방식으로 함수를 만들게 되면 함수의 클로저 또한 생성되지 않는다.
여러 가지로 미루어 보았을 때, 생성자 함수 방식으로 함수를 만드는 것은 권장되지 않는다.
자바스크립트의 eval
만큼이나 실제로 코딩에서 사용되지 않는 방법이라고 볼 수 있다.
4.2.1.5. 화살표 함수
const add = (a, b) => {
return a + b;
}
const add = (a, b) => a + b
화살표 함수는 앞서 언급한 함수 생성 방식과 몇 가지 큰 차이점이 있다.
4.2.1.5.1 화살표 함수에서는 constructor 를 사용할 수 없다.
const Car = name => {
this.name = name
}
// Uncaught TypeError: Car is not a constructor
const myCar = new Car('하이')
4.2.1.5.2 화살표 함수에서는 arguments 가 존재하지 않는다.
function hello () {
console.log(arguments)
}
// Arguments(3) [1, 2, 3 callee: f, Symbol(Symbol.iterator): f]
hello(1, 2, 3)
const hi = () => {
console.log(arguments)
}
// Uncaught ReferenceError: arguments is not defined
hi(1, 2, 3)
4.2.1.5.3 this 바인딩
이 차이로 인해 클래스형 컴포넌트에서 이벤트에 바인딩할 메서드 선언 시 화살표 함수로 했을 때와 일반 함수로 했을 때 서로 다르게 작동한다.
먼저 this
에 대해 간단하게 소개하자면, 자신이 속한 객체나 자신이 생성할 인스턴스를 가리키는 값이다.
이 this
는 화살표 함수 이전까지는 함수를 정의할 때 결정되는 것이 아니라, 함수가 어떻게 호출되느냐에 따라 동적으로 결정된다.
만약 함수가 일반 함수로서 호출된다면, 그 내부의 this
는 전역 객체를 가리키게 된다.
function a () {
console.log(this)
}
a() // Window { ... }
그러나 이와 달리 화살표 함수는 함수 자체의 바인딩을 갖지 않는다.
화살표 함수 내부에서 this
를 참조하면 상위 스코프의 this
를 그대로 따르게 된다.
이에 대한 예제를 리액트의 클래스형 컴포넌트 기반으로 살펴보자.
// 클래스형 컴포넌트에서 일반 함수와 화살표 함수로 state를 갱신하는 예제
class Component extends React.Component {
constructor(props) {
super(props);
this.state = {
counter: 1,
}
}
functionCountUp() {
console.log(this) // undefined
this.setState(prev => ({ counter: prev.counter + 1 }))
}
ArrowFunctionCountUp = () => {
console.log(this) // class Component
this.setState(prev => ({ counter: prev.counter + 1 }))
}
render() {
return (
<div>
{/* Cannot read properties of undefined (reading 'setState') */}
<button type="button" onClick={this.functionCountUp}>일반 함수</button>
{/* 정상적으로 작동한다. */}
<button type="button" onClick={this.ArrowFunctionCountUp}>화살표 함수</button>
</div>
)
}
}
위 두 메서드 functionCountUp
과 ArrowFunctionCountUp
는 모두 state
를 하나씩 올리는 작업을 동일하게 하고 있다.
그러나 일반 함수에서의 this
는 undefined
를, 화살표 함수에서의 this
는 우리가 원하는 대로 클래스의 인스턴스인 this
를 가리키는 것을 볼 수 있다.
즉, 별도의 작업을 추가로 하지 않고 this
를 접근할 수 있는 방법이 바로 화살표 함수인 것이다.
이러한 차이점은 바벨에서도 확인할 수 있다.
// 바벨 트랜스파일링으로 확인해볼 수 있는 화살표 함수와 일반 함수의 차이
// before:
const hello = () => {
console.log(this)
}
function hi() {
console.log(this)
}
// after: 바벨에서는 이렇게 변환한다.
var _this = void 0
var hello = function hello() {
// 바벨에서 화살표 함수 내부의 _this 자체를 undefined로 바꿔버린다.
console.log(_this)
}
function hi() {
console.log(this)
}
화살표 함수는 this
가 선언되는 시점에 이미 상위 스코프로 결정돼 있어 미리 _this
를 받아서 사용하는 모습과 다르게, 일반 함수는 호출하는 런타임 시점에 결정되는 this
를 그대로 따르는 모습이다.
이처럼 화살표 함수의 this
는 선언 시점에 결정된다는 일반 함수와 대비되는 큰 차이점이 있기 때문에 단순히 일반 함수의 축약형이라고 보기엔 무리가 있다.
따라서 화살표 함수와 일반 함수를 사용할 때, 특히 this
를 사용할 수 밖에 없는 클래스형 컴포넌트 내부에서 각별한 주의가 필요하다.
4.2.2 다양한 함수 살펴보기
4.2.2.1 즉시 실행 함수 (Immediately Invoked Function Expression, IIFE)
함수를 정의하고 그 순간 즉시 실행되는 함수를 의미한다.
단 한번만 호출되고, 다시금 호출할 수 없는 함수다.
// 즉시 실행 함수
(function (a, b) {
return a + b
})(10, 24); // 34
((a, b) => {
return a + b
})(10, 24) // 34
즉시 실행 함수는 한 번 선언하고 호출된 이후부터는 더 이상 재호출이 불가능하다.
그래서 일반적으로는 즉시 실행 함수에 이름을 붙이지 않는다.
이러한 즉시 실행 함수의 특성을 활용하면 글로벌 스코프를 오염시키지 않는 독립적인 함수 스코프를 운용할 수 있다는 장점을 얻을 수 있다.
함수의 선언과 실행이 바로 그 자리에서 끝나기 때문에 즉시 실행 함수 내부에 있는 값은 그 함수 내부가 아니고서는 접근이 불가능하기 때문이다.
또한 코드를 읽는 이로 하여금 이 함수는 어디서든 다시금 호출되지 않는다는 점을 각인시킬 수 있어 리팩터링에도 매우 도움이 된다.
일단 선언돼 있으면 어디서 쓸지 모르는 일반 함수와는 다르게, 즉시 실행 함수는 그 선언만으로도 실행이 거기서 끝난다는 것을 각인시킬 수 있기 때문이다.
재사용되지 않는 함수이고, 단 한번만 실행되고 끝난다면 즉시 실행 함수의 사용을 검토해보자.
4.2.2.2 고차 함수
자바스크립트의 함수가 일급 객체라는 특징을 활용하면 함수를 인수로 받거나 결과로 새로운 함수를 반환시킬 수 있다.
이런 역할을 하는 함수를 고차 함수(Higher Order Function)라고 한다.
다음 예제 코드를 살펴보자.
// 함수를 매개변수로 받는 대표적인 고차 함수, Array.prototype.map
const doubleArray = [1, 2, 3].map(item => item * 2)
doubleArray // [2, 4, 6]
// 함수를 반환하는 고차 함수의 예
const add = function (a) {
// a가 존재하는 클로저를 생성
return function (b) {
// b를 인수로 받아 두 합을 반환하는 또 다른 함수를 생성
return a + b
}
}
add(1)(3) // 4
이러한 특징을 활용해 추후에 다룰 함수형 컴포넌트를 인수로 받아 새로운 함수형 컴포넌트를 반환하는 고차 함수를 만들 수도 있다.
이런 컴포넌트를 고차 함수와 유사하게 고차 컴포넌트(Higher Order Component)라고 부르는데, 고차 함수형 컴포넌트를 만들면 컴포넌트 내부에서 공통으로 관리되는 로직을 분리해 관리할 수 있어 효율적으로 리팩토링 할 수 있다.
4.2.3 함수를 만들 때 주의해야할 사항
4.2.3.1 함수의 부수 효과를 최대한 억제하라
함수의 부수 효과(side-effect)란 함수 내의 작동으로 인해 함수가 아닌 함수 외부에 영향을 끼치는 것을 의미한다.
이러한 부수 효과가 없는 함수를 순수 함수라 하고, 부수 효과가 존재하는 함수를 비순수 함수라고 한다.
즉, 순수 함수는 부수 효과가 없고, 언제 어디서나 어떠한 상황에서든 동일한 인수를 받으면 동일한 결과를 반환해야 한다.
그리고 이러한 작동 와중에 외부에 어떠한 영향을 미쳐서는 안된다.
function pureComponent(props) {
const {a, b} = props;
return <div>{a + b}</div>
}
위 컴포넌트는 앞선 기준에 따라 순수한 함수형 컴포넌트로 분류할 수 있다.props
의 값을 기준으로 a, b를 더하고, 그 결과를 HTMLDivElement
로 렌더링하고 있다.
외부에 어떤 영향을 미치지도 않았고, 언제 어디서든 동일한 인수를 받아서 동일한 결과를 반환하기 때문에 순수한 함수형 컴포넌트라 볼 수 있다.
순수한 함수는 언제 실행되든 항상 결과가 동일하기 때문에 예측 가능하며 안정적이라는 장점이 있다.
4.2.3.2 그렇다면 어떻게서든 항상 순수 함수로만 작성해야 할까?
그렇지 않다.
웹 애플리케이션을 만드는 과정에서 부수 효과는 어떻게 보면 피할 수 없는 요소다.
컴포넌트 내부에서 API를 호출한다면 어떨까?
외부에 어떠한 영향(HTTP request)을 끼쳤으므로 부수 효과다.console.log
또한 브라우저의 콘솔 창이라는 외부에 영향을 미쳤으므로 부수 효과다.
HTML 문서의 title을 바꿨다면 이 또한 외부에 영향을 미쳤으므로 부수 효과다.
이러한 부수 효과는 웹 애플리케이션 개발에 있어 피할 수 없는 요소 중 하나다.
부수 효과를 만드는 것은 애플리케이션을 만들면서 피할 수 없는 요소지만 이러한 부수 효과를 최대한 억제할 수 있는 방향으로 함수를 설계해야 한다.
리액트의 관점에서 본다면 부수 효과를 처리하는 훅인 useEffect
의 작동을 최소화하는 것이 그 일환이라 할 수 있다.useEffect
의 사용은 피할 수 없지만 최소한으로 줄임으로써 함수의 역할을 좁히고, 버그를 줄이며, 컴포넌트의 안정성을 높일 수 있다.
따라서 자바스크립트 함수에서는 가능한 한 부수 효과를 최소화하고, 함수의 실행과 결과를 최대한 예측 가능하도록 설계해야 한다.
예측 가능한 단위의 부수 효과가 작은 함수를 설계하면 개발자와 이를 유지보수하는 또 다른 개발자에게 많은 도움을 준다.
4.2.3.3 가능한 한 함수를 작게 만들어라
자바스크립트 개발자들이 프로젝트를 만들 때 사용하는 ESLint에는 max-lines-per-function
이라는 규칙이 있다.
여기에 있는 표현을 빌리자면, 함수당 코드의 길이가 길어질수록 코드 냄새(문제를 일으킬 여지가 있는 코드)가 날 확률이 커지고, 내부에서 무슨 일이 일어나는지 추적하기 어려워진다.
이 규칙에서는 기본값으로 50줄 이상이 넘어가면 과도하게 커진 함수로 분류하고 경고 메시지를 출력한다.
그 외에도 중첩이 얼마나 많이 있고 콜백은 얼마나 많은지도 이 규칙에서 확인이 가능하다.
이 규칙의 요점은 간단하다.
하나의 함수에 너무나 많은 일을 하지 않게 하는 것이다.
유닉스의 선구자인 더글러스 매킬로이가 말한 것처럼, 함수는 하나의 일을, 그 하나만 잘하면 된다.
그것이 함수으 ㅣ원래 목적인 재사용성을 높일 수 있는 방법이다.
그렇다면 함수의 가독성을 높일 수 있는 최적의 함수 길이는 얼마일까?
ESLint의 max-lines-per-function
기본값처럼 50줄인지, 혹은 20줄인지, 몇 줄이 코드 냄새를 줄일 수 있는 최적의 함수 크기를 단언할 수 없으며, 이는 코드나 프로젝트가 처한 상황에 따라 다를 것이다.
다만, 그러한 구체적인 '큰 함수의 크기'를 정의하는 것은 불필요하다 할지라도, 가능한 한 함수의 크기를 작게 하는 것이 좋다.
4.2.3.4 누구나 이해할 수 있는 이름을 붙여라
함수나 변수에 이름을 붙이는 건 시간이 갈수록 어려워지는 문제다.
나만 보고 이해하는 프로젝트를 만들 때, 혹은 프로젝트 첫 삽을 뜨고 얼마 되지 않았을 때에는 크게 문제가 되지 않지만, 점차 코드가 커지고 비즈니스 로직이 들어가는 코드가 많아질수록 어려움에 빠지게된다.
클린 코드나 리팩터링 등에서도 많이 언급되는 문제지만, 가능한 한 함수 이름은 간결하고 이해하기 쉽게 붙이는 것이 좋다.
본인이 사용하는 프로젝트의 프레임워크에 Terser
가 설치돼 있다면 한글로 네이밍하는 것도 좋은 방법이 될 수 있다.Terser
는 자바스크립트 코드를 맹글링(mangling, 코드를 컴파일러가 이해할 수 잇는 수준에서 단순화) 및 압축하는 도구다.https://try.tersr.org/
를 방문해서 한번 테스트해보면 한글로 변수명이나 함수명을 작성해도 최종 결과물에는 크게 문제가 없다는 사실을 알게될 것이다.
// edit terser options
{
module: true,
compress: {},
mangle: {},
output: {},
parse: {},
rename: {},
}
function 보험료_계산(보험료, 햇수) {
return 보험료 * 햇수
}
const 결과 = 보험료_계산(1, 10)
console.log(결과)
실제 개발 단의 코드 크기나 번들링 속도에 있어서는 약간의 부담이 될 수 있지만, 실제 서비스되는 코드에는 영향을 미치지 않고, 또 무엇보다 한글이 편한 한국 개발자들에게 함수나 변수 네이밍에 있어 자유를 준다는 점에서 고려해 볼 만하다.
물론 Terser
가 작동하지 않는 경우도 있으므로 실제 번들링된 결과를 주의깊게 살펴볼 필요가 있다.
또한 리액트에서 사용하는 useEffect
나 useCallback
등의 훅에 넘겨주는 콜백 함수에 네이밍을 붙여준다면 가독성에 도움이 된다.
useEffect(function apiRequest() {
// ... do something
}, [])
물론 위와 같이 useEffect
의 콜백 함수에 이름을 붙여준다고 한들 apiRequest()
와 같은 형태로 호출하거나 접근할 수 있는 것은 아니다.
그러나 useEffect
같은 부수 효과를 일으키는 함수가 많아질수록 굳이 useEffect
코드를 유심히 살펴보지 않더라도 어떤 일을 하는지, 또 어떻게 작동하는지를 단번에 알아채는 데 도움이 될 것이다.
또한 이후에 살펴볼 크롬 디버깅에서도 네이밍이 돼 있는 편이 추후에 디버깅하는 데 많은 도움이 된다.