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

리액트 딥다이브 스터디를 현재 진행하고 있습니다. 이 내용을 포스팅하여 공유하고자 합니다.

1.1 자바스크립트의 동등 비교

1.1.1 자바스크립트의 데이터 타입

자바스크립트의 모든 값은 데이터 타입을 가지고 있으며, 원시 타입과 객체 타입으로 나눌 수 있습니다.

원시 타입
- boolean
- null
- undefined
- number
- string
- symbol
- bigint
 
객체 타입
- object

원시 타입이란 객체가 아닌 모든 타입을 의미하며 메서드를 갖지 않는다.

  • 메서드란 ? 객체에 포함된 함수로, 객체의 동작을 정의하며 데이터를 조작하거나 특정 작업을 수행할 수 있게 해줍니다.
const data = {
  name: "John",
  greet: function () {
    console.log("Hello, " + this.name);
  },
};
 
data.greet(); // "Hello, John"

undefined

  • 선언한 후 값을 할당하지 않은 변수 또는 값이 주어지지 않은 인수에 자동으로 할당되는 값
  • null과 undefined는 각각 null, undefined라는 값만 가질 수 있으며 그 밖의 타입은 가질 수 있는 값이 두 개 이상 존재한다.
let foo;
 
typeof foo === "undefined"; // true
 
function bar(hello) {
  return hello;
}
 
typeof bar() === "undefined"; // true

null

아직 값이 없거나 비어 있는 값을 표현할 때 사용한다.

typeof null === "object"; // true

null은 object라는 결과가 반환되며 undefined는 '선언됐지만 할당되지 않은 값', null은 '명시적으로 비어 있음을 나타내는 값'으로 사용하는 것이 일반적이다.

undefeind와 null의 차이를 보여주는 예시 코드

// undefined 예시
let undefinedVariable;
console.log(undefinedVariable); // undefined
 
// null 예시
let nullVariable = null;
console.log(nullVariable); // null
 
// 함수에서 undefined 반환
function doNothing() {
  // 아무것도 반환하지 않음
}
console.log(doNothing()); // undefined
 
// 객체에서 undefined와 null의 차이
const obj = {
  a: undefined,
  b: null,
};
 
console.log(obj.a); // undefined
console.log(obj.b); // null
 
// typeof 연산자 사용
console.log(typeof undefinedVariable); // "undefined"
console.log(typeof nullVariable); // "object"
 
// 엄격한 비교
console.log(undefinedVariable === undefined); // true
console.log(nullVariable === null); // true
 
// 느슨한 비교 (둘 다 "값이 없음"을 의미하기 때문에 true)
console.log(undefinedVariable == nullVariable); // true
 
// 엄격한 비교 (타입이 다르기 때문에 false)
console.log(undefinedVariable === nullVariable); // false

boolean

  • 참과 거짓만을 가질 수 있는 데이터 타입이다.
  • true, false와 같은 boolean 형의 값 외에도 조건문에서 마치 true와 false처럼 취급되는 truthy, falsy값이 존재한다.
console.log(typeof false, false, !!false); // "boolean false false"
console.log(typeof 0, 0, !!0); // "number 0 false"
console.log(typeof "", "", !!""); // "string  false"
console.log(typeof null, null, !!null); // "object null false"
console.log(typeof undefined, undefined, !!undefined); // "undefined undefined false"
console.log(typeof NaN, NaN, !!NaN); // "number NaN false"
 
// Truthy 값 예시
console.log(typeof true, true, !!true); // "boolean true true"
console.log(typeof 42, 42, !!42); // "number 42 true"
console.log(typeof "hello", "hello", !!"hello"); // "string hello true"
console.log(typeof {}, {}, !!{}); // "object {} true"
console.log(typeof [], [], !![]); // "object [] true"

!! 연산자를 사용하는 이유는 값을 명시적으로 boolean 타입으로 변환하여 값을 보여주기 위함입니다.

number

  • -(2^53 - 1) ~ 2^53 - 1 사이의 값을 저장할 수 있습니다.
  • 2진수, 8진수, 16진수 등의 별도 데이터 타입을 제공하지 않으므로 각 진수로 값을 표현해도 모두 10진수로 해석되어 값으로 표시됩니다.

BigInt

  • number가 다룰 수 있는 숫자 크기의 제한을 극복하기 위해 ES2020에서 새롭게 나온 타입

string

  • ', ", `(문자열 리터럴 백틱)으로 표현할 수 있습니다.
  • 백틱은 따옴표와 달리 줄바꿈을 할 수 있고 문자열 내부에 표현식을 쓸 수 있습니다.
  • 문자열은 원시 타입이며 변경 불가능합니다. 문자열이 한번 생성되면 그 문자열은 변경할 수 없습니다.
const foo = "bar";
 
console.log(foo[0]); // 'b'
 
foo[0] = "a";
 
console.log(foo[0]); // bar

Symbol

  • 중복되지 않은 고유한 값을 나타내기 위해서 사용되며 반드시 Symbol()을 사용해야 합니다.
const key = Symbol('key')
const key2 = Symbol('key')
 
key === key2 // false
 
// 동일한 값을 사용하기 위해서는 Symbol.for를 활용
Symbol.for('hello') === Symbol.for('hello) // true

객체 타입

  • 앞서 7가지의 원시 타입 이외의 모든 타입은 다 객체 타입입니다. 배열, 함수, 정규식, 클래스 등이 있습니다.

  • 객체 타입은 참조를 전달한다고 해서 참조 타입으로도 불립니다.

1.1.2 값을 저장하는 방식의 차이

  • 원시 타입과 객체 타입의 가장 큰 차이점은 값을 저장하는 방식의 차이입니다.
  • 원시 타입은 불변 형태의 값으로 저장되며 이 값은 변수 할당 시점에 메모리 영역을 차지하고 저장됩니다.
  • 객체는 프로퍼티를 삭제, 추가, 수정할 수 있으므로 원시 값과 다르게 변경 가능한 형태로 저장되며 복사할 때도 값이 아닌 참조를 전달하게 됩니다.

프토퍼티란? : 객체의 속성을 나타내는 키-값 쌍입니다. 여러 개의 프로퍼티를 가질 수 있으며, 각 프로퍼티는 key, value로 구성됩니다. 프로퍼티는 추가, 삭제, 수정할 수 있으므로 변경 가능한(mutable) 형태로 저장됩니다.

원시 타입 예시

let a = 10;
let b = a; // 값 복사
b = 20;
 
console.log(a); // 10
console.log(b); // 20
 
let str1 = "Hello";
let str2 = str1; // 값 복사
str2 = "World";
 
console.log(str1); // "Hello"
console.log(str2); // "World"

객체 타입 예시

let obj1 = { name: "John" };
let obj2 = obj1; // 참조 복사
obj2.name = "Doe";
 
console.log(obj1.name); // "Doe"
console.log(obj2.name); // "Doe"
 
// 배열
let arr1 = [1, 2, 3];
let arr2 = arr1; // 참조 복사
arr2.push(4);
 
console.log(arr1); // [1, 2, 3, 4]
console.log(arr2); // [1, 2, 3, 4]
 
// 함수
function greet() {
  console.log("Hello");
}
 
let greetCopy = greet; // 참조 복사
greetCopy(); // "Hello"
 
// 정규식
let regex1 = /hello/;
let regex2 = regex1; // 참조 복사
console.log(regex2.test("hello")); // true
 
// 클래스
class Person {
  constructor(name) {
    this.name = name;
  }
}
 
let person1 = new Person("Alice");
let person2 = person1; // 참조 복사
person2.name = "Bob";
 
console.log(person1.name); // "Bob"
console.log(person2.name); // "Bob"

객체는 값을 저장하는 것이 아니라 참조를 저장하기 때문에 동일하게 선언한 객체라고 하더라도 다른 참조를 바라보기 때문에 비교 시 false를 반환합니다.

// 두 개의 동일한 내용을 가진 객체를 생성
let obj1 = { name: "Alice", age: 25 };
let obj2 = { name: "Alice", age: 25 };
 
// 두 객체의 비교
console.log(obj1 === obj2); // false (서로 다른 참조를 가리키기 때문에)
 
// 같은 객체를 가리키는 두 변수를 생성
let obj3 = obj1;
 
// 같은 객체를 가리키는 두 변수의 비교
console.log(obj1 === obj3); // true (같은 참조를 가리키기 때문에)

1.1.3 Object.is

Object.is는 두 개의 인수를 받으며 인수 두 개가 동일한지 확인하고 반환하는 메서드입니다.

-0 === +0; // true
Object.is(-0, +0); // false
 
Number.NaN === NaN; // false
Object.is(Number.NaN, NaN); // true
 
NaN === 0 / 0; // false
Object.is(NaN, 0 / 0); // true

하지만 객체 비교에 있어서는 ===와 동일하게 작동합니다. 왜냐면 두 방법 모두 객체의 참조를 비교하기 때문입니다.

1.2 함수

1.2.1 함수란 무엇인가?

작업을 수행하거나 값을 계산하는 등의 과정을 표현하고, 이를 하나의 블록으로 감싸서 실행 단위로 만들어 놓은 것을 의미합니다.

1.2.2 함수를 정의하는 4가지 방법

함수 선언문

function add(a, b) {
  return a + b;
}

함수 선언문은 표현식이 아닌 일반 문으로 분류됩니다. 함수 선언으로는 어떠한 값도 표현되지 않았으므로 문으로 분류됩니다. 하지만

const sum = function sum(a, b) {
  return a + b;
};
 
sum(10, 24);

위 예제는 sum이라는 변수에 함수 sum을 할당하는 표현식과 같은 작동을 보였다. 이는 JS 엔진이 문맥에 따라 동일한 함수를 문이 아닌 표현식으로 해석하는 경우가 있기 때문입니다. 즉 선언문으로도 표현식으로도 사용될 수 있습니다.

함수 표현식

  • 자바스크립트에서 함수는 일급 객체이다. 일급 객체란 다른 함수의 매개변수가 될 수도 있고, 반환값이 될 수도 있으며 할당도 가능하다.
// 함수 선언
function sayHello() {
  console.log("Hello!");
}
 
// 함수 할당
const greet = sayHello;
 
// 함수 호출
greet(); // "Hello!"

함수 표현식과 선언 식의 차이

  • 가장 큰 방식의 차이는 호이스팅 여부입니다.

호이스팅이란? 호이스팅은 자바스크립트의 기본 동작 방식으로 변수 선언과 함수 선언이 실제 코드 실행 전에 해당 스코프의 최상위로 끌어올려지는 것을 의미합니다. 변수 초기화나 함수 표현식의 값은 호이스팅되지 않습니다.

hello(); // hello
 
function hello() {
  console.log("hello");
}
 
hello(); // hello

함수 선언문이 미리 메모리에 등록됐고 코드의 순서에 상관없이 정상적으로 함수를 호출할 수 있게 되었습니다.

console.log(typeof hello === "undefined"); // true
 
hello();
 
var hello = function () {
  console.log("hello");
};
 
hello();

함수 선언문과 다르게 정상적으로 호출되지 않고 undefined로 남아있습니다. 함수와 다르게 변수는 undefined로 초기화되고 할당문이 실행되는 시점, 런타임 시점에 함수가 할당되어 작동합니다.

화살표 함수

function이라는 키워드 대신 화살표를 활용하여 함수를 만든다.

const add = (a, b) => {
  return a + b;
};

화살표 함수에는 constructor를 사용할 수 없습니다.

const Car = (name) => {
  this.name = name;
};
 
const myCar = new Car("하이");

화살표 함수에는 arguments가 존재하지 않습니다.

function hello {
  console.log(arguments)
}
 
hello(1, 2, 3) // Arguments(3) [1, 2, 3, callee: ƒ, Symbol(Symbol.iterator): ƒ]
 
const hi = () => {
  console.log(arguments)
}
 
hi(1, 2, 3); // Uncaught ReferenceError: arguments is not defined

this란? 자신이 속한 객체나 자신이 생성할 인스턴스를 가리키는 값, 이는 this는 화살표 함수 이전까지는 일반 함수로서 호출된다면, 그 내부의 this는 전역 객체를 가리키게 됩니다.

const obj = {
  name: "John",
  regularFunction: function () {
    console.log(this); // obj를 가리킴
  },
  arrowFunction: () => {
    console.log(this); // 전역 객체(global object)를 가리킴
  },
};
 
obj.regularFunction(); // obj 출력
obj.arrowFunction(); // 전역 객체 출력

1.2.3 다양한 함수 살펴보기

즉시 실행 함수

한 번 선언하고 호출된 이후부터는 더 이상 재호출이 불가능한 함수

(function (a, b) {
  return a + b;
})(10, 24);

고차 함수

함수를 인수로 받거나 결과로 새로운 함수를 반환시킬 수 있습니다. 이런 역할을 하는 함수를 고차 함수라고 한다.

const doubledArray = [1, 2, 3].map((item) => item * 2);
console.log(doubledArray); // [2, 4, 6]
 
const add = function (a) {
  // a가 존재하는 클로저를 생성
  return function (b) {
    return a + b;
  };
};
 
add(1)(3); // 4

클로저란 ? 함수가 선언된 렉시컬 환경을 기억하고 그 환경에 접근할 수 있는 함수를 말합니다. 클로저는 함수가 자신이 선언된 위치에서의 스코프를 기억하고,그 스코프에 정의된 변수들에 접근할 수 있게 합니다.

렉시컬 환경이란 ? 실행 컨텍스트에서 변수를 저장하고 참조하는 구조를 정의한 개념 즉 코드가 실행될 때 변수가 어디서 정의되고 어디에서 접근할 수 있는지를 추적하는 구조입니다.

function outerFunction() {
  let outerVariable = "Outer";
 
  function innerFunction() {
    let innerVariable = "Inner";
    console.log(outerVariable); // 'Outer' 출력
    console.log(innerVariable); // 'Inner' 출력
  }
 
  innerFunction();
 
  // 아래 줄은 오류를 발생시킵니다. 외부 함수는 내부 함수의 변수를 접근할 수 없습니다.
  // console.log(innerVariable); // ReferenceError: innerVariable is not defined
}
 
outerFunction();

큰 상자(outerFunction) 안에 outerVariable이 있고 작은 상자(innerFunction)는 큰 상자 안에 있으며 이 안에는 innerVariable이 있다고 가정하였을 때 innerFunction은 outerFunction안에 있다는 것을 알고 있기에 큰 상자 안에 내용을 볼 수 있지만(접근 가능) 반대는 불가능 합니다.

함수의 부수 효과를 최대한 억제하라

함수 내의 작동으로 인해 함수 외부에 영향을 끼치는 것을 함수의 부수 효과라 합니다.(side-effect) 이러한 부수 효과가 없는 함수를 순수 함수라 하고 결과가 항상 동일하기 때문에 예측 가능하며 안정적이라는 장점이 있습니다.

리액트의 관점으로 본다면 useEffect의 작동을 최소화 하는 것이 그 일환이라 할 수 있습니다. 이러한 부수 효과를 최소화 하도록 설계해야 합니다.

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

하나의 함수에서 너무나 많은 일을 하지 않게 해야 합니다. 재사용성을 그래야만 높일 수 있습니다.

1.3 클래스

1.3.1 클래스란 무엇인가?

특정한 객체를 만들기 위한 템플릿과 같은 개념으로 볼 수 있다. 클래스를 활용하면 객체를 만드는 데 필요한 데이터나 이를 조작하는 코드를 추상화해 객체 생성을 더욱 편리하게 할 수 있습니다.

constructor

constructor는 생성자로, 객체를 생성하는 데 사용하는 특수한 메서드이며 여러 개를 사용하면 에러가 발생합니다. 별다르게 수행할 작업이 없다면 생략해도 무방합니다.

class Car {
  constructor(name) {
    this.name = name;
  }
 
  constructor(name) {
    this.name = name // Error
  }
}

프로퍼티

class로 인스턴스를 생성할 때 내부에 정의할 수 있는 속성값을 의미합니다.

인스턴스란? 클래스의 구체적인 실체로 클래스에 정의된 속성 값과 메서드를 가지는 실제 객체입니다.

class Car {
  constructor(make, model, year) {
    this.make = make;
    this.model = model;
    this.year = year;
  }
 
  displayDetail() {
    console.log(`${this.year} ${this.make} ${this.model}`);
  }
}
 
// 인스턴스 생성
const myCar = new Car("현대", "SONATA", 2023);
myCar.displayDetail(); // 2023 현대 SONATA

getter와 setter

getter란 클래스에서 무언가를 가져올 때 사용되며 setter는 클래스 필드에 값을 할당할 때 사용합니다.

class Car {
  constructor(name) {
    this.name = name;
  }
 
  get firstCharacter() {
    return this.name[0];
  }
 
  set firstCharacter(char) {
    this.name = [char, ...this.name.slice(1)].join("");
  }
}
 
const myCar = new Car("자동차");
 
console.log(myCar.firstCharacter); // 자
 
myCar.firstCharacter = "차";
 
console.log(myCar.firstCharacter, myCar.name); // 차 차동차

인스턴스 메서드

클래스 내부에서 선언한 메서드를 인스턴스 메서드라고 합니다. 이는 prototype에 선언되므로 프로토타입 메서드로 불리기도 합니다.

class Car {
  constructor(name) {
    this.name = name;
  }
 
  // 인스턴스 메서드 정의
  hello() {
    console.log(`안녕하세요, ${this.name}입니다.`);
  }
}

이는 다음과 같이 선언할 수 있습니다.

const myCar = new Car("자동차");
myCar.hello(); // 안녕하세요, 자동차입니다.

직접 객체에서 선언하지 않았음에도 프로토타입에 있는 메서드를 찾아서 실행을 도와주는 것을 프로토타입 체이닝이라고 합니다. 이를 통해서 최상위 객체인 Object까지 확인하여 hello를 호출할 수 있게 됩니다.

const myCar = {
  name: "자동차",
 
  // 객체에 직접 메서드 정의
  hello: function () {
    console.log(`안녕하세요, ${this.name}입니다.`);
  },
};
 
myCar.hello(); // 안녕하세요, 자동차입니다.

정적 메서드

class Car {
  static hello() {
    console.log("안녕하세요!");
  }
}
 
const myCar = new Car();
 
myCar.hello(); // Error
Car.hello(); // 출력: 안녕하세요!

클래스의 인스턴스가 아닌 이름으로 호출할 수 있는 메서드이며 내부의 this는 생성된 인스턴스가 아닌 클래스 자신을 가리키기 때문에 사용할 수 없습니다.

애플리케이션 전역에서 사용하는 유틸 함수를 정적 메서드로 많이 활용합니다.

1.4 클로저

함수와 함수가 선언된 렉시컬 스코프의 조합

렉시컬 스코프는 변수가 코드 내부에서 어디서 선언 되었는지를 말합니다.호출되는 방식에 따라서 동적으로 결정되는 this와 다르게

렉시컬 스코프는 코드가 작성되는 순간 정적으로 결정됩니다.

const globalVar = "global";
 
function outerFunction() {
  const outerVar = "outer";
 
  function innerFunction() {
    const innerVar = "inner";
    console.log(globalVar); // "global"
    console.log(outerVar); // "outer"
    console.log(innerVar); // "inner"
  }
 
  innerFunction();
}
 
outerFunction();

전역 상자 > outerFunction 상자 > innerFunction 상자 순으로 되어 있다고 생각하면 편합니다.

안에선 밖을 접근할 수 있지만 바깥에선 안을 접근할 수 없습니다.

function Counter() {
  let counter = 0;
  return {
    increase: function () {
      var increaseVar = 5;
      return ++counter;
    },
    decrease: function () {
      var decreaseVar = 4;
      return --counter;
    },
    counter: function () {
      var counterVar = 6;
      console.log("couter에 접근!");
      return counter;
    },
  };
}
 
let c = Counter();
 
console.log(c.increase()); // 1
console.log(c.increase()); // 2
console.log(c.increase()); // 3
console.log(c.decrease()); // 2
console.log(c.counter()); // counter에 접근! 2

Counter 함수는 counter라는 0으로 시작하는 변수를 하나 만듭니다. 또한 그 안에는 increase, decrease, counter라는 상자가

Counter 안에 존재 합니다. 따라서 각각의 함수는 counter를 접근할 수 있지만 Counter에서는 각 함수에 선언된 변수에 접근할 수

없습니다.

for (var i = 0; i < 5; i++) {
  setTimeout(function () {
    console.log(i); // 5 5 5 5 5
  }, 100);
}

i는 함수 스코프를 가집니다. for 루프 안에서 선언된 i가 하나의 변수로 존재를 합니다. 즉 i는 증가를 하지만 하나의 변수로써

존재한다는 의미입니다. setTimeout 비동기가 실행되면서 i의 값은 빠르게 5가 된 이후에 출력을 하기 때문에 5 5 5 5 5가 출력됩니다.

for (let i = 0; i < 5; i++) {
  setTimeout(function () {
    console.log(i); // 0 1 2 3 4
  }, 100);
}

그에 반해 let은 블록 스코프를 가집니다. 즉 새로운 i의 인스턴스가 생성이 되고 표시되는 i의 값은 각각이 다른 메모리를 참조합니다.

이러한 이유로 사용자가 기대하는 0 1 2 3 4가 출력되게 됩니다.

for (var i = 0; i < 5; i++) {
  setTimeout(
    (function (sec) {
      return function () {
        console.log(sec);
      };
    })(i),
    100
  );
}

문제

  1. 원시 타입과 객체 타입의 차이점은?

  2. 함수 선언문과 함수 표현식이 다른 것은?

  3. 클로저란?

  4. 블록 스코프랑 함수 스코프의 차이점은?