리액트 개발을 위해 꼭 알아야 할 자바스크립트 (1)

리액트 개발을 위해 꼭 알아야 할 자바스크립트 (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로 취급되는 값을 말한다.

    타입설명
    falseBooleanfalse는 대표적인 falsy한 값이다.
    0, -0, 0n, 0x0nNumber, BigInt0은 부호나 소수점 유무에 상관없이 falsy한 값이다.
    NaNNumberNumber가 아니라는 것을 뜻하는 NaN(Not a Number)은 falsy한 값이다.
    ’’, “”, ``String문자열이 falsy하기 위해서는 반드시 공백이 없는 빈 문자열이어야 한다.
    nullnullnull이 falsy한 값이다.
    undefinedundefinedundefined는 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는 두 개인 인수를 받으며, 이 인수 두개가 동일한지 확인하고 반환하는 메서드다.

  • == vs Object.is : == 비교는 강제로 형변환(type casting)을 한 후에 변경한다. Object.is는 === 는 타입이 다르면 그냥 false다.

  • === vs Object.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가 있다.

가능한 한 함수를 작게 만들어라

함수당 코드의 길이가 길어를 최소호하면 좋다.

하나의 함수에서 너무나 많은 일을 하지 않게 하는게 좋다.

함수의 원래 목적인 재사용성을 높일 수 있는 방법이다.

누구나 이해할 수 있는 이름을 붙여라

가능한 한 함수 이름은 간결하고 이해하기 쉽게 붙이는 것이 좋다.

리액트에서 사용하는 useEffectuseCallback 등의 혹에 넘겨주는 콜백 함수에 네이밍을 붙여준다면 가독성에 도움이 된다.

useEffect(function apiRequest() {
 // ... do something
}, [])

© 2021. All rights reserved.

Powered by Hydejack v9.1.6