리액트 개발을 위해 꼭 알아야 할 자바스크립트 (1)
모던 리액트 Deep Dive: 리액트의 핵심 개념과 동작 원리부터 Next.js까지, 리액트의 모든 것 - 01장 리액트 개발을 위해 꼭 알아야 할 자바스크립트
1. 자바스크립트의 동등 비교
🛠️ 참고
자바스크립트의 데이터 타입
자바스크립트의 모든 값은 데이터 타입을 갖고 있으며, 이 데이터 타입은 원시 타입과 객체 타입으로 나눈다.
원시 타입(primitive type)
- boolean
- null
- undefined
- number
- string
- symbol
- bigint
원시 타입(primitive type)
- object
원시 타입
자바스크립트에서 원시 타입이란 간단히 정의하자면 객체가 아닌 다른 모든 타입을 의미한다.
undefined
undefined는 선언한 후 값을 할당하지 않은 변수 또는 값이 주어지지 않은 인수에 자동으로 할당되는 값이다.
let foo
type foo === 'undefined' // true
function bar(hello) {
return hello
}
typeof bar() === 'undefined' // true
null
아직 값이 없거나 비어 있는 값을 표현할 때 사용한다.
typeof null === 'object' // true?
null이 가지고 있는 특별한 점 하나는 다른 원시값과 다르게 typeof로 null을 확인했을 때 해당 타입이 아닌 ‘object’라는 결과가 반환된다는 것이다.
Boolean
참(true)과 거짓(false)만을 가질 수 있는 데이터 타입이다.
조건문에서 마치 true와 false처럼 취급되는 truthy, falsy 값이 존재한다.
falsy: 조건문 내부에서 flase로 취급되는 값을 말한다.
값 타입 설명 false Boolean false는 대표적인 falsy한 값이다. 0, -0, 0n, 0x0n Number, BigInt 0은 부호나 소수점 유무에 상관없이 falsy한 값이다. NaN Number Number가 아니라는 것을 뜻하는 NaN(Not a Number)은 falsy한 값이다. ’’, “”, `` String 문자열이 falsy하기 위해서는 반드시 공백이 없는 빈 문자열이어야 한다. null null null이 falsy한 값이다. undefined undefined undefined는 falsy한 값이다. truthy: 조건문 내부에서 true로 취급되는 값, 앞아서 언급한 falsy로 취급되는 값 이외에는 모두 true로 취급된다.
즉, {}, [] 모두 truthy한 값이다.
if (1) { // true } if (0) { // false } if (NaN) { // false } // 조건문 외에도 truthy와 falsy를 Boolean()을 통해 확인할 수도 있다. Boolean(1) // true Boolean(2) // false Boolean(true) // true
Number
정수와 실수를 구분해 저장하는 다른 언어와 다르게, 자바스크립트는 모두 숫자를 하나의 타입에 저장했었다.
const a = 1;
const maxInteger = Math.pow(2, 53);
maxInteger - 1 === Number.MAX_SAFE_INTEGER // true
const minInteger = -(Math.pow(2, 53) - 1);
minInteger === Number.MIN_SAFE_INTEGER // true
진수별로 값을 표현해도 모두 10진수로 해석되어 동일한 값으로 표시된다.
Bigint
최대 2^53 - 1을 저장할 수 있는 number의 한계를 넘어서 더 큰 숫자를 저장할 수 있게 해준다.
// 기존 number의 한계
9007199254740992 === 9007199254740993 // 마지막 숫자는 다른데 true가 나온다. 이는 더 이상 다룰 수 없는 크기이기 때문이다.
const maxInteger = Number.MAX_SAFE_INTEGER;
console.log(maxInteger + 5 === maxInteger + 6) // true??
const bigInt1 = 9007199254740995n // 끝에 n을 붙이거나
const bigInt2 = BigInt('9007199254740995') // BigInt 함수를 사용하면 된다.
const number = 9007199254740992;
const bigint = 9007199254740992n;
typeof number; // number
typeof bigint; // bigint
number == bigint; // true
number === bigint; // false (타입이 달라서 false가 반환된다.)
String
string은 텍스트 타입의 데이터를 저장하기 위해 사용된다.
- 작음따옴표
'
- 큰따옴표
"
- 백틱 ` `
으로 표현할 수 있다.
// '\n안녕하세요.\n'
const longText = `
안녕하세요.
`
//Uncaught SyntaxError: Invalid or unexpected token
const longText = "
안녕하세요.
"
문자열은 원시 타입이며 변경 불가능하다는 것이다.
const foo = 'bar'
console.log(foo[0]) // 'b'
// 앞 글자를 다른 글자로 변경해 보았다.
foo[0] = 'a'
// 이는 반영되지 않는다.
console.log(foo) // bar
Symbol
Symbol은 중복되지 않는 어떠한 고유한 값을 나타내기 위해 만들어졌다.
심벌을 생성하려면 반드시 Symbol()을 사용해야만 한다.
// Symbol 함수에 같은 인수를 넘겨주더라도 이는 동일한 값으로 인정되지 않는다.
// 심벌 함수 내부에 넘겨주는 값을 Symbol 생성에 영향을 미치지 않는다 (Symbol.for 제외)
const key = Symbol('key');
const key2 = Symbol('key')
key === key2 // false
// 동일한 값을 사용하기 위해서는 Symbol.for을 활용한다.
Symbol.for('hello') === Symbol.for('hello') // true
객체 타입
객체 타입을 간단하게 정의하면 앞서 7가지 원시 타입 이외의 모든 것, 즉 자바스크립트를 이루고 있는 대부분의 타입이 바로 객체 타입이다.
typeof [] === 'object' // true
typeof {} === 'object' // true
function hello() {}
typeof hello === 'function' // true
const hello1 = function() {
}
const hello2 = function() {
}
// 객체인 함수의 내용이 육안으로는 같아 보여도 참조가 다르기 때무에 false가 반환된다.
hello1 === hello2 // false
값을 저장하는 방식의 차이
원시 타입과 객체 타입의 가장 큰 차이점으라고 한다면. 바로 값을 저장하는 방식의 차이다.
먼저 원시 타입은 불편 형태의 값으로 저장된다. 그리고 이 값은 변수 할당 시점에 메모리 영역을 차지하고 저장한다.
let hello = 'hello world' let hi = hello console.log(hello === hi) // true
값을 비교하기 때문에 동일한 결과를 볼 수 있다.
let hello = 'hello world' let hi = 'hello world' console.log(hello === hi) // true
반면 객체는 변경 가능한 형태로 저장되며, 값을 복사할 때도 값이 아닌 참조를 전달하게 된다.
// 다음 객체는 완벽하게 동일한 내용을 가지고 있다.
var hello = {
greet: 'hello, world',
}
var hi = {
greet: 'hello, world',
}
// 그러나 동등 비교를 하면 false가 나온다.
console.log(hello === hi) // false
// 원시값이 내부 속성값을 비교하면 동일하다.
console.log(hello.greet === hi.greet) // true
객체는 값을 저장하는게 아니라 참조를 저장한다.
값은 같았을지언정 참조하는 곳이 다르다.
hello와 hi 변수는 변수명 및 각 변수명의 주소가 서로 다르지만 value가 가리키는 주소가 동일하다.
var hello = { greet: 'hello, world', } var hi = hello console.log(hi === hello) // true
객체 간의 비교는 우리가 이해하는 내부의 값이 같다하더라도 결과는 대부분 true가 아닐 수 있다.
자바스크립트의 또 다른 비교 공식, Object.is
Object.is는 두 개인 인수를 받으며, 이 인수 두개가 동일한지 확인하고 반환하는 메서드다.
==
vsObject.is
:==
비교는 강제로 형변환(type casting)을 한 후에 변경한다. Object.is는===
는 타입이 다르면 그냥 false다.===
vsObject.is
:Object.is
가 좀 정확한 비교를 한다.-0 === +0; // true Object.is(-0, +0) // false Number.NaN === NaN; // false Objectis(Number.NaN, NaN) // true NaN === 0 / 0 // false Object.is(NaN, 0 / 0) // true
Object.is
를 사용한다 하더라도 객체 비교에는 별 차이가 없다..
Object.is({}, {}) // false
const a = {
hello: 'hi,
}
const b = a;
Object.is(a, b); // true
a === b // true
리액트에서의 동등 비교
리액트에서 사용하는 동등 비교는 Object.is
다. Object.is
는 ES6에서 제공하는 기능이기 때문에 리액트에서는 이를 구현한 폴리필(Polyfill)을 함께 사용한다.
https://github.com/facebook/react/blob/main/packages/shared/objectIs.js 에서 확인
리액트에서는 이 ObjectIs를 기반으로 동등 비교를 하는 shallowEqual이라는 함수를 만들어 사용한다.
https://github.com/facebook/react/blob/main/packages/shared/shallowEqual.js 에서 확인
리액트에서의 비교를 요약하자면 Object.is
로 먼저 비교를 수행한 다음에 Object.is
에서 수행하지 못하는 비교(객체 간 얕은 비교)를 한 번 더 수행하는 것을 알 수 있다.
// Object.is는 참조가 다른 객체에 대해 비교가 불가능하다.
Object.is({hello: 'world'}, {hello: 'world'}) // false
// 반면 리액트 팀에서 구현한 shallowEqual은 객체의 1 depth까지는 비교가 가능하다.
shallowEqual({hello: 'world'}, {hello: 'world'}) // true
// 그러나 2 depth까지 가면 이를 비교할 방법이 없으므로 false를 반환한다.
shallowEqual({hello: {hi: 'world'}}, {hello: {hi: 'world'}}) // false
이렇게 객체의 얕은 비교까지만 구현한 이유는 리액트에서 사용하는 JSX props는 객체이고, 그리고 여기에 있는 props만 일차적으로 비교하면 되기 때문이다.
type Props = {
hello: string
}
function HelloComponent(props:Props) {
return <h1>{hello}</h1>
}
// ...
function App() {
return <HelloComponent hello="hi!" />
}
리액트는 props에서 꺼내온 값을 기준으로 렌더링을 수행하기 때문에 일반적인 케이스에서는 얕은 비교로 충분할 것이다.
import {memo, useEffect, useState } from "react"
type Props = {
counter: number
}
const Component = memo((props: Props) => {
useEffect(()=>{
console.log('Component has been readered!')
})
return <h1>{props.counter}</h1>
})
type DeeperProps = {
counter: {
counter: number
}
}
const DeeperComponents = memo((props: DeeperProps) => {
useEffect(()=> {
console.log('Deeper Components has been redered!')
})
return <h1>{props.counter.counter}</h1>
})
export default function App() {
const [, setCouner] = useState(0)
function handleClick() {
setCounter((prev) => prev + 1)
}
return (
<div className="App">
<Component counter={100} />
<DeeperComponent counter= />
<button onClick={handleClick}>+</button>
</div>
)
}
props가 깊어지는 경우, 즉 한 객체 안에 또다른 객체가 있을 경우 React.memo는 컴포넌트에 실제로 변경된 값 없음에도 불구하고 메모이제이션된 컴포넌트를 반환하지 못한다.
2. 함수
🛠️ 참고
함수란 무엇인가?
자바스크립트에서 함수란 작업을 수행하거나 값을 계산하는 등의 과정을 표현하고, 이를 하나의 블록으로 감싸서 실행 단위로 만들어 놓은 것을 의미한다.
function sum(a, b) {
return a + b
}
sum(10, 24) // 34
function
뒤에 오는 것이 함수명함수의 입력값으로 받는 a, b를 각각 매개변수
return
으로 작성된 것이 반환값이다.sum 뒤에 넘겨준 두 개의 값 10과 24를 인자라고 한다.
function Component(props) {
return <div>{props.hello}</div>
}
Component
라고 하는 함수명일반적으로 props라고 부르는 단일 객체를 받는 매개변수
return 문으로 JSX를 반환한다.
함수를 정의하는 4가지 방법
함수 선언문
function add(a, b) {
return a + b
}
함수 선언문은 표현식이 아닌 일반 문(statement)으로 분류한다.
const sum = function sum(a, b) {
return a + b
}
sum(10, 24) // 34
함수 표현식
프로그래밍 세계에서 일급 객체란 다른 객체들에 일반적으로 적용 가능한 연산을 모두 지원하는 객체를 의미한다.
자바스크립트에서 함수는 일급 객체다.
const sum = function add(a, b) {
// 함수 몸통에서 현재 실행 중인 함수를 참조하는 데 사용할 수 있다.
// 이는 단순히 코드에 대한 이해를 돕기 위한 예제 코드고,
// 실제 프로덕션 코드에서는 절대로 사용해서는 안 된다.
console.log(arguments.callee.name)
return a + b
}
sum(10, 24)
//add
add(10, 24) // Uncaught TypeError: add is not defined
함수 표현식과 선언 식의 차이
함수 표현식과 함수 선언 식의 가장 큰 차이점은 호이스팅(hoising)이다.
호이스팅
함수의 호이스팅이라 함은, 함수 선언문이 마치 코드 맨 앞단에 작성된 것처럼 동작하는 자바스크립트의 특징을 의미한다.
hello() // hello
function hello() {
console.log('hello')
}
hello() // hello
함수의 호이스팅은 함수에 대한 선언을 실행 전에 미리 메모리에 등록하는 적업을 의미한다. 이러한 함수의 호이스팅이라는 특징 덕분에 함수 선언문이 미리 메모리에 등록됐고, 코드의 순서에 상관없이 정상적으로 함수를 호출할 수 있게 된 것이다.
console.log(typeof hello === 'undefined') // true
hello() // Uncaught TypeError: hello is not a function
var hello = function () {
console.log('hello')
}
hello()
함수와 다르게 변수는, 런타임 이전에 undefined로 초기화되고, 할당문이 실행되는 시점, 런타임 시점에 함수가 할당되어 작동한다는 것이다.
Function 생성자
const add = new Function('a', 'b', 'return a + b')
add(10, 24) // 34
매개변수, 그리고 함수의 몸통을 모두 문자열로 작성해야 한다.
화살표 함수
function이라는 키워드 대신 =>
라는 화살표를 활용해서 함수를 만든다.
const add = (a, b) => {
return a + b
}
const add = (a, b) => a + b
화살표 함수는 함수 생성 방식과 몇 가지 차이점이 있다.
화살표 함수에서는 constructor를 사용할 수 없다.
const Car = (name) => { this.name = name } // uncaught TypeError: Car is not a constructor const myCar = new Car('하이')
화살표 함수에서는 arguments가 존재하지 않는다.
function hello() { console.log(arguments) } // Argument(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)
화살표 함수에서는 함수 자체의 바인딩을 갖지 않는다.
화살표 함수 내부에서 this를 참조하면 상위 스코프인 this를 그대로 따르게 된다.
class Component extends React.Component { constructor(props) { super(props) this.state = { conter: 1, } } functionCountUp() { console.log(this) // undefined this.setState((prev) => ({counter: prev.counter + 1})) } ArrorFunctionCountUp = () => { console.log(this) // class Component this.setState((prev) => ({counter: prev.counter + 1})) } render() { return ( <div> {/* Cannot read properties of undefined (reading 'setState') */} <button onClick={this.functionCountUp}>일반 함수</button> {/* 정상적으로 작동한다. */} <button onClick={this.ArrorFunctionCountUp}>화살표 함수</button> </div> ) } }
별도의 작업을 추가로 하지 않고 this를 접근할 수 있는 방법이 바로 화살표 함수인 것이다. 이러한 차이점은 바벨에서도 확인할 수 있다.
고차 함수
앞서 언급했듯이 자바스크립트 함수가 일급 객체라는 특징을 할용하면 함수를 인수로 받거나 결과로 새로운 함수를 반환시킬 수 있다. 이런 역할을 하는 함수를 고차 함수(Higher Order Function)라고 한다.
// 함수를 매개변수로 받는 대표적인 고차 함수, Arror.prototype.map
const doubledArray = [1, 2, 3].map((item) => item * 2)
doubledArray // [2, 4, 6]
// 험수를 반환하는 고차 함수의 예
const add = function(a) {
// a가 존재하는 클로저를 생성
return function (b) {
// b를 인수로 받아 두 합을 반환하는 또 다른 함수를 생성
return a + b
}
}
add(1)(3) // 4
컴포넌트를 고차 함수와 유사하게 고차 컴포넌트(Higher Order Component)라고 부르는데, 고차 함수형 컴포넌트를 만들면 컴포넌트 내부에서 공통으로 괸리되는 로직을 분리해 관리할 수 있어 효율적으로 리팩토링 할 수 있다.
함수를 만들 때 주의해야 할 사항
함수의 부수 효과를 최대한 억제하라
함수의 부수 효과(side-effect)란 함수 내의 작동으로 인해 함수가 아닌 함수 외부에 영향을 끼치는 것을 의미한다.
부수 효과가 없는 함수를 순수 함수
부수 효과가 존재하는 함수를 비순수 함수
순수 함수는 부수 효과가 없고, 언제 어디서 어떠한 상황에서도 동일한 인수를 받으면 동일한 결과를 반환해야 한다.
function PureComponent(props) {
const {a, b} = props
return <div>{a + b}</div>
}
리액트에서 부수 효과를 처리하는 훅인 useEffect가 있다.
가능한 한 함수를 작게 만들어라
함수당 코드의 길이가 길어를 최소호하면 좋다.
하나의 함수에서 너무나 많은 일을 하지 않게 하는게 좋다.
함수의 원래 목적인 재사용성을 높일 수 있는 방법이다.
누구나 이해할 수 있는 이름을 붙여라
가능한 한 함수 이름은 간결하고 이해하기 쉽게 붙이는 것이 좋다.
리액트에서 사용하는 useEffect
나 useCallback
등의 혹에 넘겨주는 콜백 함수에 네이밍을 붙여준다면 가독성에 도움이 된다.
useEffect(function apiRequest() {
// ... do something
}, [])