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

1.5 이벤트 루프와 비동기 통신의 이해 1.6 리액트에서 자주 사용하는 자바스크립트 문법 1.7 선택이 아닌 필수, 타입스크립트
강석우's avatar
Feb 25, 2024
1장 리액트 개발을 위해 꼭 알아야 할 스크립트 (3)

1.5. 이벤트 루프와 비동기 통신의 이해

자바스크립트는 싱글 스레드에서 작동한다.
한번에 하나의 작업만 동기 방식으로만 처리할 수 있다. 하지만 웹사이트를 살펴보면 유저는 하나의 작업 이후 기다리고 다음 작업을 하는 것이 아니라 동시에 여러 작업을 진행할 수 있다.
어떤 원리로 작동이 되는지 알아보자

1.5.1 싱글 스레드 자바스크립트

프로세스란 프로그램을 구동해 프로그램의 상태가 메모리상에서 실행되는 작업단위.
하나의 프로세스는 여러개의 스레드. 스레드끼리는 메모리를 공유할 수 있어 여러 가지 작업을 동시에 수행 가능하다.
하지만 자바스크립트는 싱글스레드 이기 때문에 순차적으로 작업이 실행된다.

1.5.2 이벤트 루프

이벤트 루프란 자바스크립트 런타임 외부에서 자바스크립트의 비동기 실행을 돕기 위해 만들어진 장치라 볼 수 있다.

호출 스택과 이벤트 루프

호출 스택(call stack)은 자바스크립트에서 수행해야 할 코드나 함수를 순차적으로 담아두는 스택이다.
이벤트 루프는 이 호출스택이 비어있는지 확인하는 것이다.
 

아래의 코드는 동기 작업의 진행이다.

function bar() {
  console.log('bar')
}

function baz() {
  console.log('baz')
}

function foo() {
  console.log('foo')
  bar()
  baz()
}

foo()
  1. foo()가 호출 스택에 먼저 들어간다.

  2. foo() 내부에 console.log가 존재하므로 호출 스택에 들어간다.

  3. 2의 실행이 완료된 이후에 다음 코드로 넘어간다. (아직 foo()는 존재)

  4. bar()가 호출 스택에 들어간다.

  5. bar() 내부에 console.log가 존재하므로 호출 스택에 들어간다.

  6. 5의 실행이 완료된 이후에 다음 코드로 넘어간다. (아직 foo(), bar()는 존재)

  7. 더 이상 bar()에 남은 것이 없으므로 호출 스택에서 제거된다. (아직 foo()는 존재)

  8. baz()가 호출 스택에 들어간다.

  9. baz() 내부에 console.log가 존재하므로 호출 스택에 들어간다.

  10. 9의 실행이 완료된 이후에 다음 코드로 넘어간다. (아직 foo(), baz()는 존재)

  11. 더 이상 baz()에 남은 것이 없으므로 호출 스택에서 제거된다. (아직 foo()는 존재)

  12. 더 이상 foo()에 남은 것이 없으므로 호출 스택에서 제거된다.

  13. 13. 이제 호출 스택이 완전히 비워졌다.

이벤트 루프는 단일 스레드에서 호출 스택 내부에 수행해야 할 작업이 있는지 확인하고, 수행해야 할 코드가 있다면 자바스크립트 엔진을 이용해 실행한다.

아래는 비동기 작업의 진행이다.

function bar() {
  console.log('bar')
}

function baz() {
  console.log('baz')
}

function foo() {
  console.log('foo')
  setTimeout(bar(), 0) // setTimeout만 추가했다.
  baz()
}

foo()

  1. foo()가 호출 스택에 먼저 들어간다.

  2. foo() 내부에 console.log가 존재하므로 호출 스택에 들어간다.

  3. 2의 실행이 완료된 이후에 다음 코드로 넘어간다. (아직 foo()는 존재)

  4. setTimeout(bar(), 0)이 호출 스택에 들어간다.

  5. 4번에 대해 타이머 이벤트가 실행되며 태스크 큐로 들어가고, 그 대신 바로 스택에서 제거

  6. baz()가 호출 스택에 들어간다.

  7. baz() 내부에 console.log가 존재하므로 호출 스택에 들어간다.

  8. 7의 실행이 완료된 이후에 다음 코드로 넘어간다. (아직 foo(), baz()는 존재)

  9. 더 이상 baz()에 남은 것이 없으므로 호출 스택에서 제거된다. (아직 foo()는 존재)

  10. 더 이상 foo()에 남은 것이 없으므로 호출 스택에서 제거된다

  11. 이제 호출 스택이 완전히 비워졌다.

  12. 이벤트 루프가 호출 스택이 비워져 있다는 것을 확인했다. 그리고 태스크 큐를 확인하니 4번에 들어갔던 내용이 있어 bar()를 호출 스택에 들여보낸다.

  13. bar() 내부에 console.log가 존재하므로 호출 스택에 들어간다.

  14. 13의 실행이 끝나고, 다음 코드로 넘어간다. (아직 bar() 존재)

  15. 더 이상 bar()에 남은 것이 없으므로 호출 스택에서 제거된다.

태스크 큐 : 실행해야 할 태스크의 집합

이벤트 루프의 역할은 호출 스택에 실행 중인 코드가 있는지, 그리고 태스크 큐에 대기 중인 함수가 있는지 반복해서 확인하는 역할을 한다. 호출 스택이 비었다면 태스크 큐에 대기 중인 작업이 있는지 확인하고, 이 작업을 실행 가능한 오래된 것부터 순차적으로 꺼내와서 실행하게 된다. 이 작업 또한 마찬가지로 태스크 큐가 빌 때까지 이루어진다.

정리하자면 비동기 함수들은 자바스크립트 코드가 동기식으로 실행되는 메인 스레드가 아닌 태스크 큐가 할당되는 별도의 스레드에서 수행된다.
자바스크립트의 코드 실행은 자바스크립트의 싱글스레드에서 이루어지지만 외부 Web API 등은 모두 자바스크립트 코드 외부에서 실행되고 콜백이 태스크 큐로 들어가는 것이다.
이벤트 루프는 호출 스택이 비고, 콜백이 실행 가능한 때가 오면 이것을 꺼내서 수행하는 역할을 한다.

1.5.3 태스크 큐와 마이크로 태스크 큐

마이크로 태스크 큐는 기존 태스크 큐보다 우선권을 갖는다.
마이크로 태스크 큐에는 대표적으로 Promise 가 있다.
즉 setTimeout 과 setInterval 은 Promise보다 늦게 실행된다.

function foo() {
 console.log('foo')
}

function bar() {
 console.log('bar')
}

function baz() {
 console.log('baz')
}

setTimeout(foo, 0)
Promise.resolve().then(bar).then(baz)

위 코드의 경우 bar, baz, foo 순서로 작업이 실행된다.

1.6. 리액트에서 자주 사용하는 자바스크립트 문법

1.6.1 구조 분해 할당

구조 분해 할당(Destructuring assignment)이란 배열 또는 객체의 값을 말 그대로 분해해 개별 변수에 즉시 할당하는 것을 의미한다.
아래는 예시 코드이다.

배열 구조 분해 할당

const array = [1, 2, 3, 4, 5]
const [first, , , , fifth] = array 

first // 1
fifth // 5
const array = [1, 2, 3, 4, 5]

const [first, ...rest] = array 
  • 중간 인덱스에 대한 할당 생략 가능

  • 기본값 선언 가능

  • undefined일 때 기본값이 사용됨.

  • 특정값 이후의 값을 배열로 선언하고 싶다면 전개 연산자 사용 가능
    하지만 순서가 중요하다. 전개 연산자 뒤에 변수 추가로 사용 불가!

객체 구조 분해 할당

객체 구조 분해 할당은 객체에서 값을 꺼내온 뒤 할당하는 것을 의미

const object = {
 a: 1,
 b: 2,
 c: 3,
 d: 4,
 e: 5,
}

const { a, b, c, ...objectRest } = object
const object = {
 a: 1,
 b: 2,
}

const { a: first, b: second } = object
const key = 'a'
const object = {
 a: 1,
 b: 1,
}

const { [key]: hello } = object
  • 새로운 이름으로 다시 할당 가능

  • 기본값 주기 가능

  • 변수에 있는 값으로 꺼내오는 계산된 속성 이름 장식 가능
    네이밍 해주지 않으면 에러가 난다.

1.6.3 전개 구문

배열의 전개 구문

합성

const arr1 = ['a', 'b']
const arr2 = [...arr1, 'c', 'd', 'e'] // ['a', 'b', 'c', 'd', 'e']

복사

전개 연산자를 사용해 배열을 복사할 경우 참조가 아닌 내용을 복사한다.

const arr1 = ['a', 'b']
const arr2 = arr1
arr1 === arr2 // true

const arr1 = ['a', 'b']
const arr2 = [...arr1]
arr1 === arr2 // false

객체의 전개 구문

합성

const obj1 = {
 a: 1,
 b: 2,
}

const obj2 = {
 c: 3,
 d: 4,
}

const newObj = { ...obj1, ...obj2 }

하지만 객체의 합성의 경우 순서가 매우 중요하다. 주의하도록 하자.

const obj = {
 a: 1,
 b: 1,
 c: 1,
 d: 1,
 e: 1,
}
// {a: 1, b: 1, c: 10, d: 1, e: 1}

const aObj = {
 ...obj,
 c: 10,
}

// {c: 1, a: 1, b: 1, d: 1, e: 1}
const bObj = {
 c: 10,
 ...obj,
}

객체 연산자는 번들링 시에 크기가 커지기 때문에 꼭 필요할 때만 사용하도록 하자.

1.6.3 객체 초기자

원래대로라면 a: a와 같은 형식으로 작성해야 했었는데, 넣어야 할 키와 값이 각각 a와 1이고, 이미 해당 내용으로 선언된 변수가 있다면 아래와 같은 형식으로 축약해서 선언하는 것이 가능해진 것이다.

const a = 1
const b = 2
const obj = {
 a,
 b,
}
// {a: 1, b: 2}

1.6.4 Array 프로토타입의 메서드: map, filter, reduce, forEach

Array.prototype.map

인수로 전달받은 배열과 같은 길이의 새로운 배열을 반환하는 메서드.
배열의 각 아이템을 순회하며 콜백으로 연산한 결과로 새로운 배열을 만든다.

const arr = [1, 2, 3, 4, 5]
const doubledArr = arr.map((item) => item * 2)

Array.prototype.filter

콜백 함수를 인수로 받으며 콜백 함수의 truthy 조건을 만족하는 원소만 반환하는 메서드.
원본 배열의 길이 이하의 새로운 배열이 반환된다.

const arr = [1, 2, 3, 4, 5]
const evenArr = arr.filter((item) => item % 2 === 0) // [2,4]

Array.prototype.reduce

콜백 함수와 함께 초기 값을 추가로 인수를 받는데, 이 초깃 값에 따라 배열이나 객체, 또는 그 외의 다른 무언가를 반환할 수 있는 메서드.
콜백함수를 실행하고, 이를 초깃값에 누적해 결과를 반환한다.

const arr = [1, 2, 3, 4, 5]
const sum = arr.reduce((result, item) => {
 return result + item
}, 0) // 15
// 0 : 초깃값, result : 초깃값의 현재값, item : 현재 배열의 아이템

filter와 map 을 reduce 하나로 작성이 가능하다.

// 짝수만 100을 곱해 반환하는 함수의 예제
const arr = [1, 2, 3, 4, 5]
// [200, 400]

const result1 = arr.filter((item) => item % 2 === 0).map((item) => item * 100)
// [200, 400]

const result2 = arr.reduce((result, item) => {
 if (item % 2 === 0) {
  result.push(item * 100)
  } 
  return result
}, [])

Array.prototype.forEach

콜백 함수를 받아 배열을 순회하면서 단순히 그 콜백 함수를 실행한다.
주의할 점은 콜백함수를 "실행" 할 뿐 그 어떠한 값도 반환하지 않는다.
또한 forEach는 실행되는 순간 에러를 던지거나 프로세스를 종료하지 않는 이상 멈출 수 없다.

const arr = [1, 2, 3]
arr.forEach((item) => console.log(item))
// 1, 2, 3

1.6.5 삼항 조건 연산자

자바스크립트에서 유일하게 3개의 피연산자를 취할 수 있는 문법.

조건문 ? 참일 때의 값 : 거짓일 때의 값

삼항연산자를 중복해서 사용할 경우 코드를 읽기 매우 어려워지므로 사용하지 말도록 하자.

삼항연산자를 사용하지 않고도 JSX 내부에서 조건부 렌더링을 구현할 수 있다.

<div>
  {(() => {
   if (color === 'red') {
    return '빨간색이다.'
   } else {
    return '빨간색이 아니다.'
   }
  })()}
</div>

위의 코드는 동작은 하지만 가독성을 해치고 불필요하게 즉시 실행함수를 선언해서 사용해야 하기 때문에 선호되지 않는다.

1.7. 선택이 아닌 필수, 타입스크립트

1.7.1 타입스크립트란?

“TypeScript is JavaScript with syntax for types.”
자바스크립트는 동적 타입의 언어이기 때문에 대부분의 에러를 코드를 실행했을 때만 확인할 수 있다.

정적 타입언어 : 컴파일시에 타입을 설정하는 것
동적 타입언어 : 코드 실행시에 타입을 설정하는 것

1.7.2 리액트 코드를 효과적으로 작성하기 위한 타입스크립트 활용법

any 대신 unknown을 사용하자

any는 타입스크립트의 이점을 모두 버리는 것이나 다름없다.
자바스크립트에서 타입스크립트로 넘어가는 과도기같이 정말 예외적인 경우에만 any를 사용하도록 하자. 만약 타입을 단정할 수 없다면 unkown을 사용하자.

function doSomething(callback: unknown) {
 if (typeof callback === 'function') {
  callback()
  return
 }
 throw new Error('callback은 함수여야 합니다.')
}

위의 케이스에서 if typeof 를 사용해주지 않는다면 타입 오류가 나게 된다.

never에 대해서는 자세히 설명해 놓은 블로그가 있다. https://ui.toast.com/posts/ko_20220323

타입 가드를 적극 활용하자

instanceof 는 지정한 인스턴스가 특정 클래스의 인스턴스인지 확인할 수 있는 연산자.
typeof 연산자는 특정 요소에 대해 자료형을 확인하는 데 사용된다.
in은 어떤 객체에 키가 존재하는지 확인하는 용도로 사용된다.

function doSchool(person: Student | Teacher) {
 if ('age' in person) {
  person.age // person은 Student
  person.score
 }

 if ('name' in person) {
  person.name // person은 Teacher
 }
}

제네릭

함수나 클래스 내부에서 단일 타입이 아닌 다양한 타입에 대응할 수 있도록 도와주는 도구.
들어온 타입을 그대로 내보내준다고 생각하면 된다.

function getFirstAndLast<T>(list: T[]): [T, T] {
 return [list[0], list[list.length - 1]]
}

const [first, last] = getFirstAndLast([1, 2, 3, 4, 5])

first // number
last // number

const [first, last] = getFirstAndLast(['a', 'b', 'c', 'd', 'e'])

first // string
last //string

인덱스 시그니처

객체의 키를 정의하는 방식을 의미한다.

type Hello = {
 [key: string]: string
}

const hello: Hello = {
 hello: 'hello',
 hi: 'hi',
}

hello['hi'] // hi
hello['안녕'] // undefined

위의 [key: string] 부분이 인덱스 시그니처다. 인덱스 시그니처를 사용하면 키에 원하는 타입을 부여할 수 있다. 동적인 객체를 정의할 때 유용하지만, 키의 범위가 앞선 예제의 경우 string으로 너무 커지기 때문에 객체의 키는 동적으로 선언되는 경우를 최대한 지양하고 타입도 필요에 따라 좁혀야 한다.

// record를 사용
type Hello = Record<'hello' | 'hi', string>

const hello: Hello = {
 hello: 'hello',
 hi: 'hi',
}


// 타입을 사용한 인덱스 시그니처
type Hello = { [key in 'hello' | 'hi']: string }

const hello: Hello = {
 hello: 'hello',
 hi: 'hi',
}

위와 같은 방법으로 객체를 원하는 형태로 좁혀야한다.

1.7.3 타입스크립트 전환 가이드

읽고 넘어가도록 하자

Share article
RSSPowered by inblog