리액트의 소스코드나 알고리즘 문제의 풀이를 볼 때마다 javascript 코드의 arrow function의 실행문의 결과를 이해하기 힘들어 정리해보았습니다.
기본적인 형태
() => { return value; }
가장 기본적인 형태입니다.
const getOne = () => { return 1; };
console.log(getOne()); // 1 출력
const getHello = () => { return {hello: 'world'}; };
console.log(getHello()); // {hello: 'world'} 출력
() => value
value의 자리에는 {}
로 감싸지는 literal object 형식을 제외한 값이 모두 들어갈 수 있습니다. literal object가 안되는 이유는 뒤에서 다루도록 하겠습니다.
const getOne = () => 1;
console.log(getOne()); // 1 출력
const getOne = () => (1);
console.log(getOne()); // 1 출력
오브젝트를 {}로 감싸는 형태로 반환한다고 생각해보면,() => obj
와 같이 구성할 수도 있을 것입니다.
const printHello = () => { hello: 'world' }
console.log(b2()); //undefined
그런데, 오브젝트 단독으로 반환되는 경우는 () => { return value; }
와 형태가 겹칩니다. 코드 블럭을 의미하는 {}를 의도치 않게 오브젝트를 반환할 때도 똑같은 형태로 사용했습니다. 이렇게 된다면 컴파일러는 {}를 코드 블럭으로 해석하고, 코드 블럭 안에서는 return
문이 없기에 아무것도 반환이 되지 않습니다. 따라서 undefined가 반환됩니다.
const printHello = () => ({hello: 'world'});
console.log(printHello()); // 출력: { hello: world }
() => (value)
의 형태로 작성한다면 오브젝트를 반환할 때 () => ({ ... })
과 같은 형태로 들어가므로 오브젝트로 컴파일러가 인지하여 의도하던대로 오브젝트가 반환됩니다.
우선 표현식이 무엇인지부터 정의하고 넘어갑니다.
표현식은 값이 결정될 수 있는 유요한 코드의 단위이다. - mdn
쉽게 말하자면, 모든 자바스크립트의 모든 데이터 및 수식이 표현식(expression)입니다. primitive values, referenced values, 연산자와 피연산자로 구성된 식, 함수 호출문 등이 모두 expression입니다. 예로 들자면,
console.log("hello") // undefined
printOne() // 1
1 + 2 // 3
3.141592 // 3.141592
{ 1: '초라도 안보이면', 2: '이렇게 초조한데', 3: '초는 어떻게 기다려'}
[ '너와', '나의', '연결', '고리' ]
이것들이 모두 expression 입니다.
console.log
는 built-in 함수인데 반환값 자체가 없잖아? 반환값이 곧 함수의 결정되는 값일텐데... 그러면 console.log는 표현식이 아니잖아?
이러한 궁금증이 생길수도 있습니다. 궁금증을 해결하기 위해 console.log 함수 자체가 무엇을 반환하는지 한번 코드로 확인합시다.
function returnConsoleLog(){
return console.log();
}
console.log(returnConsolelog());
그러면 개행이 한번 일어나고 undefined
가 출력됩니다. 개행은 console.log()
함수를 실행할 때 발생한 것이고, undefined는 console.log()
의 반환값입니다. undefined도 반환값의 일종이므로 console.log()
는 결국 반환값이 있고, 값이 결정됩니다. 따라서 expression이라고 할 수 있습니다. 이런식으로 반환값이 없을거라고 예상되는 함수들은 보통 undefined
가 반환됩니다.
()로 묶인 여러개의 expression은 어떻게 실행될까요?
console.log((2, 3, 4, console.log("hello buddy"), console.log("hello javascript"), 5));
// 출력:
// hello buddy
// hello javascript
출력 결과를 통해 다음을 추정할 수 있습니다.
이제 arrow function에서 어떻게 expresssion을 사용하는지 확인해봅시다.
중요: javascript에서는 하나의 expression이 화살표(=>
) 다음에 오는 것만 허용하고 있습니다.
() => expression
기본적인 형태입니다. expression을 실행합니다.
const printHello = () => console.log("hello");
printHello(); // "hello" 출력
printHello
)의 반환값이 됩니다.const printHello = () => console.log("hello");
console.log(printHello()); // undefined 출력
printHello 함수가 호출되어 expression에 해당하는 console.log("hello")
가 실행된 뒤에 console.log("hello")
의 반환값을 출력하는 코드입니다. console.log
함수의 반환값은 undefined
이므로 printHello
의 반환값도 undefined
가 되겠습니다.
() => (expression)
도 동일한 기능을 수행합니다.const printHello = () => (console.log("hello"));
printHello() // hello 출력
저는 (1)에서 이렇게 말했습니다.
중요: javascript에서는 화살표(
=>
) 다음에 하나의 expression이 오는 것만 허용하고 있습니다.
이 말을 따른다면 expression을 여러개 실행시키는 것은 코드 블럭({}
)을 쓰는 방법밖에 없을 것입니다.
const sayHellos = () => {
console.log("Hello buddy");
console.log("Hello world");
console.log("Hello javascript");
}
그러므로 다음과 같이 쓰는 것도 불가능합니다.
const sayHellos = () => console.log("Hello buddy"), console.log("Hello world"), console.log("Hello javascript");
// 에러 발생: Uncaught SyntaxError: Missing initializer in const declaration
sayHellos();
하지만, {}
없이도 여러개의 expression을 실행시키는 것이 가능합니다. 바로 ()
입니다. 소괄호는 여러개의 expression을 묶어서 하나로 만들어주는 역할을 합니다. 소괄호 안에 들어있는 여러개(1개 이상)의 expression은 모두 하나로 처리됩니다. 실행 방식은 위에서 적은 것을 참고하시기 바랍니다.
예를 들어
console.log("hello");
는 하나의 expression이고,
(console.log("hello buddy"), console.log("hello javascript"));
도 하나의 expression으로 볼 수 있습니다. 따라서 다음과 같이 화살표 함수를 구성할 수 있습니다.
() => (expression1, expression2, expression3, ...)
// expression의 개수가 여러개일 경우, ','로 이어주어 차례대로 실행할 수 있다.
// 그러나 이 때에는 ()로 전체 expresssion을 감싸야 한다. expression을 묶어서 하나로 만들기 위해서는 소괄호로 묶어야 한다.
const sayHellos = () => (console.log("Hello buddy"), console.log("Hello world"), console.log("Hello javascript"));
sayHellos();
/*출력
Hello buddy
Hello world
Hello javascript
*/
이제 다음 expression 부분에서 설명했던 내용을 바탕으로 다음 코드의 실행 결과를 예측해봅시다.
const testFunc = () => (1, console.log(2), 3, console.log(4)); // 2, 4 출력 후 console.log(4)의 반환값인 undefined 반환
console.log(testFunc()) // undefined 출력
// expression이 ,로 나열되었을 경우 반환하는 값은 마지막의 value이다.
const testFunc1 = (console.log(1), 1); //1 출력 후 1 반환
console.log(testFunc1()) // 반환된 1 출력
const testFunc2 = () => (console.log("hello"), 4); // hello 출력 후 4 반환
console.log(testFunc2()) // 반환된 4 출력
인생은 실전입니다. 예제 코드를 보면서 react에도 배운 사실들을 적용시켜 봅시다. 다음에 주어진 setState 구문의 실행을 예측하여 봅시다. 두 코드의 실행 결과는 모두 같습니다.
toggleVisibility() {
this.setState(state => {
if (state.visibility === true) {
return { visibility: false };
} else {
return { visibility: true };
}
});
}
toggleVisibility() {
this.setState(state => ({
visibility: !state.visibility
}));
}
상태값은 초기값을 설정해준 뒤 컴포넌트에 특정한 이벤트가 발생할 때마다 변하는 값입니다. 아마 상태값 visiblity
는 true
또는 false
가 되도록 초기화 해주었을 것입니다. 그리고 toggleVisibility
함수가 호출되었을 경우 첫번째 코드던 두번째 코드던 state = { visiblity: !state.visibility }
를 반환해줄시 원래 state 오브젝트가 { visiblity: true }
였다면 { visiblity: false }
로, { visibility: false }
였다면 { visiblity: true }
로 바뀝니다. 결론적으로 본래 가지고 있었던 진리값의 부정(invert)이 state에 저장됩니다.
state[visibility] = !state.visibility
새로운 오브젝트를 만들어 state가 가리키에 하는 대신에 기존의 오브젝트의 property를 바꾸는 것이 효율적이지 않을까?
리액트에서 state의 값은 불변성이 핵심입니다. 과거에 있었던 state 값을 사용할 수도 있고, 새로 오브젝트를 만들면 디버깅을 할 때 어떤 단계에서 상태값이 예상되는 것과 다른지 확인할 수 있어 리액트는 상태값을 새로 만드는 방법을 사용합니다. 기존의 state를 바꾸는 방식은 리액트에서 절대로 쓰면 안됩니다.