[리액트+TS] 우아한 테크러닝 3기 1주차
Web Frontend Developer

[리액트+TS] 우아한 테크러닝 3기 1주차


나는 이번 9월 우아한형제들에서 주최하는 우아한 테크러닝 세미나를 듣기로 했다. 원래는 오프라인으로 30명만 받아서 진행하기로 했었다는데, 코로나19 때문에 온라인으로 전환이 되면서 수백명이 들을 수 있게 되었다. 나한테는 참 다행인 것 같다. ㅎㅎ

이번 온라인 세미나는 3년차 미만의 주니어 웹 프론트엔드 개발자들을 대상으로 리액트와 타입스크립트를 중심으로 주니어 웹 프론트엔드 개발자에게 필요한 기술들을 알려준다. 나 같은 경우 지금 다니고 있는 회사에서 시니어 웹 프론트엔드 개발자가 없다 보니 시니어의 관점(?)에서 웹 프론트엔드 개발을 어떻게 바라보는지가 궁금했고 배우고 싶어서 세미나를 듣게 되었다. 한달 동안 진행이 되는데, 매주 공부한 내용들을 간단하게나마 정리해 보려고 한다.

 

첫 번째 세션 9월 1일 화요일

고민

강의를 맡아주신 민태님은 먼저 온라인 세미나에 참여한 수백명은 주니어 웹 프론트엔드 개발자들이 가지고 있는 공통적인 고민들에 대해서 이야기를 시작하셨다. 특히 시니어 개발자가 없는 회사에서 일하는 주니어의 입장에서는 더더욱 그렇다.

나는 잘 하고 있나?

이러한 고민에 대해서 민태님은 여러가지 이야기를 해 주셨는데, 그 중 기억나는 것들은 다음과 같다.

도구는 만든 사람이 어떠한 목적을 가지고 만든 것인지 잘 알고 목적에 맞게 쓰자. 그리고 도구를 사용할 때 왜 이 도구를 현재 프로젝트에서 사용하는지를 반드시 고민하고 쓸것! 팀에서 협업을 하면서 그 도구의 Best Practice를 찾아서 잘 쓸지 아니면 유연성을 가져가며 넓은 활용성을 가지고 쓸지는 협업하는 사람들과 충분히 논의하면서 방향을 찾아야 할 문제이다.

그리고 많은 개발자들이 풀스택 개발자를 희망하는데, 풀스택 개발자는 개발을 오래 하다보면 자연스럽게 되는 것이 바람직하며 지향할 것은 아니다. 프론트엔드 개발을 하다 보면 서버 개발자와 협업을 많이 하게 되는데 그러면서 자연스럽게 서버쪽 지식이 늘어나게 되는 것이다. 프론트엔드/백엔드 한 분야만 파도 해야될게 너무 많은데 양쪽을 다 잡는다는 말은 둘 다 전문성을 놓치는 것을 의미한다.

자주 볼 페이지

앞으로 한 달간 배워야 할 내용들에 대한 공식 문서를 나열해 본다.

  1. TypeScript 공식 문서
  2. CodeSandbox
  3. React 공식 문서
  4. Redux 공식 문서
  5. MobX 공식 문서
  6. Redux-saga 공식 문서
  7. Blueprint.js 공식 문서 (TS 사용하면서 쓰기 좋은 example)
  8. 테스팅 라이브러리 

타입스크립트

프로그래밍 언어가 되게 많은데 각각은 별게 "있다". 그렇기 때문에 각각의 언어의 특성을 잘 알고 장점을 살려서 쓰는 것도 굉장히 중요하다. 타입스크립트는 자바스크립트에서 사람이 코드를 짜면서 타입으로 인해 발생할 수 있는 휴먼 에러를 가능한 부분은 예방해 줄 수 있다는 장점이 있는 언어이다.

타입스크립트에서 타입을 명시적으로 정해주는 것이 좋을까? 아니면 암묵적으로 정해주는 것일까? 많은 사람들은 암묵적인 것보다 명시적인 것을 선호한다. 명시적인 타입 지정은 1. 새로 코드를 보는 사람이 해당 변수가 어떤 타입을 필요로 하는지 한 번에 확인할 수 있고 2. 문제가 생겼을 때 빠르게 원인을 분석할 수 있다 는 점에서 암시적인 타입 지정보다 유리하다. 아래의 예제 처럼 암묵적인 타입 지정을 한 변수는 다른 타입의 값을 변수에 넣어줄 경우 에러가 발생한다.

let bar = 10;
bar = false; // Error : Type 'boolean' is not assignable to type 'number'.

타입스크립트에서는 컴파일 타임에서만 작동하는 요소들이 있고(Alias, Interface, Generic 등), 런타임까지 가져가는 요소들이 있다. 둘을 잘 구분해서 목적에 맞게 잘 써주는 것이 중요하다. Type Alias와 Interface는 비슷해 보이지만 다음과 같은 차이가 있다. 첫 번째로, 인터페이스는 여러 곳에서 사용되는 새로운 이름을 만들지만, 타입 알리아스는 새로운 이름을 만들지 않는다. 또한 인터페이스는 extend/implement 될 수 있지만, 타입 알리아스는 extend/implement 될 수 없다.

type Age = number;

type Foo = {
  age: Age;
  name: string;
}

interface Bar = {
  age: Age;
  name: string;
}

const foo: Foo = {
  age: 27,
  name: 'owen'
}

const bar: Bar = {
  age: 27,
  name: 'owen'
}

Create React App

리액트 개발환경은 과거에는 아주 복잡했었다. 그걸 아주 쉽게 가능하게 해준 한 줄이다. 

yarn create react-app hello-world --template typescript

CRA는 아주 간단하게 리액트 프로젝트를 시작할 수 있다는 강력한 장점이 있지만, 그 이외에는 모두 단점이라고 보면 된다. 제품이 완성 단계에서 넘어갈 때 문제가 생기기 시작한다. 그리고 다양한 환경에 고루고루 적용하기는 어려울 수 있다. 따라서 운영 모드에서는 리액트를 웹팩으로 직접 설정해서 써 주는 것을 권장한다.

간단한 리액트 컴포넌트를 다음과 같이 만들어 볼 수 있다.

import React from "react";
import ReactDom from "react-dom";

function App() {
    return (
        <h1>Tech Hello!</h1>
    );
}

ReactDom.render(
    <React.StricMode>
        <App/>
    </React.StrictMode>,
    document.getElementById("root")
)

ReactDom의 render 함수는 Virtual DOM에 컴포넌트를 그리며, 두 개의 인자를 받는다. 첫 번째 인자는 렌더링된 컴포넌트이며 두 번째 인자는 렌더링된 컴포넌트를 넣을 HTML 요소이다. 그리고 작성된 App 컴포넌트는 babel이라는 트랜스파일러를 통해 다음과 같이 변환된다.

// 변환 전
function App() {
    return <h1 id="header">Tech Hello!</h1>;
}

// 변환 후
function App() {
    return /*@__PURE__*/ React.createElement(
        "h1",
        {
            id: "header",
        },
        "Hello Tech"
    );
}

작성된 jsx 코드가 이처럼 React의 createElement 함수로 변환이 된다.

상태관리

전역에 있는 상태들을 어떻게 관리할 것인지에 대한 고민으로 현재 React의 경우 가장 많이 쓰는 도구는 Redux고 그 다음이 MobX이다. 참고로 MobX는 리덕스의 대체품이 아니라 완전히 상태관리를 하는 패러다임을 바꾼 녀석으로 여겨진다. Redux는 매우 간단하게 사용이 가능하며 그래서 쉽게 많은 사람들이 사용한다. 반면 MobX는 기능이 많고 그만큼 응용할 수 있는 범위가 넓다. 민태님의 비유를 들자면 리덕스는 마치 어린아이한테 심부름을 시키는 것과 같고, MobX는 어른에게 심부름을 시키는 것과 같다고 한다. 

 

두 번째 세션 9월 3일 목요일

자바스크립트

함수는 값이다! 그렇기 때문에 어떤 상황에서든지 값을 반환한다. 개인적으로 이 말이 제일 중요했다고 생각한다. 자바스크립트에는 공리가 하나 있는데, 그것은 바로 '변수 안에 값을 넣을 수 있다는 것'이며 함수도 여기에서 변수로 볼 수가 있다.

function foo() {
  return 0;
}

const bar = function () { // 이름이 있어도 이름을 인식하지 못하기 때문에 이름 생략 가능

};

bar();

(function() {})(); // 즉시 실행 함수

함수를 값으로 취급할 때, 우리는 함수의 이름을 생략할 수 있다. 이와 같이 이름을 생략한 함수식을 익명 함수라고 하며 이러한 함수는 때론 즉시 호출할 때 사용이 된다. 바로 호출 하고 싶을 때 이렇게 IIFE(즉시 실행 함수)를 만들어서 사용한다. 반면 재귀호출되는 함수의 이름은 생략할 수 없다.

// 함수의 매개변수와 반환값으로서의 함수

function foo(x) {
  x();
  return function() {};
}

const y = foo(function() {})

그리고 변수는 값이며 함수의 파라미터에 변수가 들어간다면 이 변수로 함수가 들어갈 수 있다. 이렇게자로 함수를 전달하는 것을 콜백함수라고 한다. 어떤 함수에게 함수 하나 줄 테니 대신 호출해 달라고 함수를 위임하는 것이다. 소프트웨어공학에서 함수를 인자로 받고 함수를 반환하는 함수를 1급 함수(High Order Function)라고 한다.

 

함수의 선언에는 크게 두 가지가 있는데 하나는 함수 정의문이고 다른 하나는 함수 표현식이다. 함수 뿐만 아니라 모든 자바스크립트 코드는 문과 식 두가지로 이루어져 있다고 한다. 식은 값으로 나타낼 수 있는 값이다. 그리고 세미콜론으로 마무리를 한다.

// 식 : 값으로 나타낼 수 있다. 세미콜론으로 마무리를 한다.
0;
1 + 10;
foo();

반면 문은 식이 아닌 모든 코드를 의미하고 여기에는 if문, for문, while문 등이 포함된다. 함수를 정의하는 문도 이 문에 들어간다.

// 함수 정의문
function 함수이름(x){
  return y;
}

// 함수 표현식
const 변수이름 = function 함수이름(x){
  return y;
}

 

ES6 이후에는 화살표 함수가 등장했으며 람다 함수라고 부르기도 한다. 화살표 함수에 대해서는 뒤에 더 자세히 다룰 예정이다.

 

this

new 연산자를 호출하면 this를 생성하며 인스턴스 객체는 생성된 this를 반환한다. new 연산자는 새로운 객체를 만들어 내는데 내부적으로는 프로토타입이라는 방식을 사용하며 이렇게 만들어진 함수를 생성자 함수라고 한다.

function foo() {
    this.age = 10;
}

const y = new foo();
console.log(y); // { age: 10, constructor: object }

생성된 인스턴스 객체 y를 출력하면 constructor가 추가되어 반환되는 것을 볼 수 있다. 생성된 객체의 타입을 확인하기 위해서는 instanceof를  'y instanceof foo'와 같이 사용할 수 있다. 이는 명시적이지 않아서 실수하기가 쉽다.

그래서 ES6 이후에는 클래스 문법을 제공하기 시작했다. 일반 함수와 비교해 보면 코드 구조적인 부분에서 class를 이용했을 때 좀 더 명시적이라는 것을 알 수 있다.

// class 사용 (ES6 이후)
class bar {
    constructor() {
        this.age = 10;
    }
}

console.log(new bar()); // { age: 10, constructor: object }


// ES5 이전
function foo() {
    this.age = 10;
}

foo(); // undefined
new foo(); // { age: 10 }

 

this가 결정되는 방식은 실행 컨텍스트로 호출 시에 결정이 된다. 선언 시점이 아닌 호출 시점임을 명심하자. 아래 예제에서는 person이 실행 컨텍스트가 되는 것이다. this에 대한 자세한 설명은 이전에 this에 대한 포스팅을 적어놓았으므로 거기를 참고하자.

const person = {
    name: "Owen",
    getName() {
        return this.name;
    },
};

console.log(person.getName()); // Owen

// 이러면 에러가 난다.
const man = person.getName;
console.log(man()); // window 객체에서 name을 찾을 수 없기 때문에 error

// 이렇게 바인딩을 해 주어야 한다.

const man = person.getName.bind(person);
console.log(man()); // Owen
person.getName.call(person); // Owen

this를 고정하기 위해서는 bind, call, apply와 같은 함수들을 사용할 수 있다. 이러한 함수들은 헬퍼 함수로서 소유자가 벗겨져도 이 안의 this가 항상 바인드에 입력된 객체가 될 수 있도록 묶어주는 역할을 한다.

 

클로저

변수의 스코프에 따라 변수는 사라지지만 함수의 실행 컨텍스트에 저장되어 내부 함수에서만 접근이 가능하게 되는 현상을 클로저(Closure)라고 한다. 즉 함수 실행결과로 반환된 함수가 외부의 스코프를 기억하고 있는 상태를 클로저라 하는 것이다.

const person = {
  age: 10
}

function makePerson() {
  let age = 10;
  return {
    getAge() {
      return age;
    },
    setAge(x) {
      age = x > 1 && x < 130 ? x : age;
    }
  }
}

person.age = 500; // age를 보호할 수 없다.

let p = makePerson();

console.log(p.getAge());
console.log(p.setAge(30)); 

위의 함수에서 makePerson안의 age에 접근할 수 있는 경로는 getAge()로 받아오거나 setAge()로 넣는 두 가지 방법밖에 없다. 이처럼 캡슐화(encapsulation)를 통해 함수 안의 변수를 보호할 수가 있다.

 

비동기

자바스크립트는 싱글 스레드 언어이다. 인간은 동기적으로 처리하는 과정이 익숙하고 비동기적인 처리는 낯설다. 예를 들면 setTimeout이나 API 호출하는 코드 등이 자바스크립트에서 비동기적으로 처리되는데, 사람의 머리로 이 과정을 이해하는 것은 처음에는 어려울 수가 있다.

비동기 로직을 깊고 복잡하게 짜다보면 콜백 지옥이 발생하고 이를 해결하기 위해 나온 것이 Promise이다. Promise에 대해서는 정리해 놓은 포스팅이 있으니 참고하자.

const p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
     resolve("응답1");
  }, 1000);

  reject();
});

const p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
     resolve("응답2");
  }, 1000);

  reject();
});

p1.then(p2())
  .then(function (r) {
     console.log(r);
  })
  .catch(function () {});

Promise의 .then() 콜백함수는 resolve()로 호출되며, .catch()로 넘겨준 콜백함수는 reject로 호출된다. Promise를 좀 더 가독성이 좋게 만든 것이 ES7의 async/await이다. 아래의 예제는 콘솔에 1이 출력된 후 3초 뒤에 2가 출력된다.

const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function main() {
  console.log("1");
  try {
     const result = await delay(3000);   
   } catch (e) {
     console.error(e);
  }
  console.log("2");
}

main();

 

Redux

민태님이 리덕스를 직접 만드는 과정을 보여주셨다. 솔직하게 고백하면 이 부분은 강의를 제대로 듣지 못했다. 그래서 리덕스에 대해서는 나중에 제대로 포스팅을 다시 한 번 해볼 생각이다.