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

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

오늘은 우아한 테크러닝 2주차(3회차, 4회차)에서 공부했던 내용들에 대해서 정리해 보려고 한다.

 

세 번째 세션 9월 8일 화요일

세 번째 시간은 React에 대해 알아보는 시간이었다. 리액트가 만들어진 이유와 가상 DOM (Virtual DOM), 그리고 간단한 실습과 리액트에서 상태관리를 어떻게 하는지에 대해서 공부했다.

과거에 우리는 직접 DOM을 조작해서 JS로 화면을 그려주었다. 여기에 사용되었던 대표적인 라이브러리가 jQuery였다.

list에 들어있는 데이터를 기반으로, rootElement의 innerHTML에 넣어 DOM을 직접 조작했다. 이 코드를 보면서 민태님이 몇 가지 조언을 해주셨는데 다음과 같다.

  • 코드는 끊임없이 변화하다 보니 좀더 변화에 잘 대응할수 있는, 빠르게 수정할 수 있는 코드가 필요하다. 코드에서 같은 것끼리 묶고, 다른 것은 분리하자. 이 원칙을 어떻게 지키고 어떠한 것이 같고 다른 것인가? 같은 것과 다른 것의 분리가 역량에 따라서 차이가 많이 난다.
  • 이름을 잘 짓는 것이 아키텍처 측면에서도 중요하다. 아키텍처에 여러가지의 좋은 영향을 주는 요소가 있다면 그 중 아주 기본적인 10가지 정도가 좋은 코드구조를 만드는데 70~80퍼센트의 영향을 준다. 나머지 20~30퍼센트는 디테일을 잡는 것이다. 너무 높은 수준을 추구하다가 기본을 소홀히 하지 않도록 한다. 항상 70퍼센트의 기본부터 충실하게 하는게 중요하다.

동일한 input이면 동일한 output이 나오는 순수함수의 형태로 만들어서 외부 의존성을 줄인 코드는 아래와 같다.

이렇게 real DOM을 직접 건드리면, real DOM은 low level이기 때문에 안정성이 떨어진다. 그리고 앱의 규모가 커질 수록 복잡도가 급격하게 늘어나게 된다. 이러한 이유로 페이스북에서 리액트를 만들었다. 리액트를 사용하게 되면 DOM을 가상 DOM으로 변환하여 조금 더 간단하게 개발자가 다룰 수 있다.

기본적인 리액트의 구조를 살펴보면 다음과 같다.

App 함수의 반환값은 JSX 문법으로 이루어져 있고, HTML 태그와 비슷하게 컴포넌트를 만든다. JSX로 리턴할 때 다른 컴포넌트를 가져와서 반환하는 것도 가능하다. 그리고 ReactDOM은 render() 라는 정적 메서드를 통해 두 개의 파라미터를 받는다. 첫번째는 화면에 렌더링할 컴포넌트이고, 두 번째는 컴포넌트를 렌더링할 요소이다. ReactDOM.render()의 역할은 가상 DOM을 훑으면서 실제 DOM을 그려주는 역할을 한다. 마치 innerHTML 처럼 말이다.

HTML에서는 class 속성을 사용하는 반면 JSX에서는 className 속성을 사용한다. className을 적용한 함수와, 그 함수의 반환값을 객체로 바꾸면 다음과 같다. 리액트는 이러한 원리를 통해 가상 DOM을 만들고 하나의 부모 아래에서 자식들을 만들어 내는 것이다.

 

리액트를 지금부터 본격적으로 만들어 보자. 리액트는 DOM - Virtual DOM - JavaScript 의 흐름에서 가상 DOM을 DOM과 JS 가운데에 두며 사용성을 용이하게 하기 위해 JSX 문법을 사용하고, 이를 바벨이 트랜스파일링 한다. 이 때 리액트의 createElement()는 가상 DOM을 만드는 역할을 한다. 그리고 최상단에 /* @jsx createElement */ 를 넣어주면 바벨로 트랜스파일링되는 가상 DOM의 역할을 줄 수 있다.

function createElement(type, props, ...children) {
    return { type, props, ...children };
}

강의 중간에 이렇게 직접 간단한 리액트를 만들고 그것을 이용한 어플리케이션을 보여주셨는데 소스 코드는 다음과 같다.

 

다음은 리액트에서 상태관리를 하는 방법이다. 리액트의 컴포넌트는 클래스형 컴포넌트와 함수형 컴포넌트가 있다. 이에 대해서는 개인적으로 별도의 포스팅에서도 정리해 논 적이 있다. 

클래스형 컴포넌트는 라이프사이클 API가 존재하며 상태를 생성자에서 초기화할 수 있다. 상태를 변경하기 위해서는 컴포넌트에 존재하는 setState 메서드를 사용한다. 클래스 컴포넌트의 상태는 객체의 생성자에 초기화되기 때문에 리액트가 지우지 않는 한 유지된다. 클래스형 컴포넌트는 상태를 바꿔주게 되면 컴포넌트에서 리렌더링이 일어난다.

함수형 컴포넌트는 초기에는 상태를 갖지 못하는 컴포넌트였다. 함수 컴포넌트의 상태는 함수가 호출될 때 마다 생성되기 때문에 유지될 수 없었기 때문이다. 함수형 컴포넌트는 그 안에서 상태를 바꾸어주는 함수를 실행하더라도 화면이 리렌더링이 되지 않는다.

그러나 작년 훅이 등장하면서 상태를 관리할 수 있게 되었다. 클래스 컴포넌트처럼 상태를 관리할 때 useState를 사용한다. useState의 반환 배열은 첫 번째는 상태값이며 두 번째는 dispatcher로 사용되는 함수이다.

훅은 전역 배열로 관리되며 생성되는 순서에 따라 컴포넌트를 key로 하여 인덱스로 관리한다. 훅을 사용하는 컴포넌트의 경우 관련 정보가 없을 경우 처음 실행되는 것으로 판단되어 초기값이 저장된다. 훅은 최상위(top level)에서만 호출하는 것을 권장하는데 최상위가 아닌 부분에서 호출될 경우 전역 배열에 문제가 발생해 원치 않는 값을 반환하는 경우가 생길 수 있기 때문이다.

 

네 번째 세션 9월 10일 목요일

네 번째 시간은 함수형 컴포넌트에서 상태와 컴포넌트의 분리, 제너레이터와 비동기 작업 등에 대해서 알아보는 시간이었다.

다음과 같은 간단한 리스트를 보여주는 리액트 어플리케이션이 있다고 생각해보자.

물론 이렇게 코드를 작성해도 돌아가는데에는 문제가 없지만, 더 좋은 아키텍처를 고민해 볼 수 있을 것 같다. 우리가 해볼 수 있는 시도는 1. 네이밍을 신경쓰고 2. 크기가 크다면 작게 쪼개고 3. 분리가 필요한 부분을 적절하게 분리해서 공통적인 부분을 묶어주는 정도로 볼 수 있을 것이다. 특히 컴포넌트를 쪼개는 고민은 임계점을 넘어가는 순간 굉장히 부담스러운 일이 될 수가 있으므로.. 필요하면 빠르게 하는 것을 권장.

위의 코드에서는 map 내부의 li 태그가 생성되는 부분은 코드가 복잡해질 경우 가독성 측면에서 좋지 않을 수 있으므로 외부 컴포넌트를 하나 더 만들어서 작업해 볼 수 있을 것 같다. 그리고 함수 컴포넌트 내부에서 목록을 정렬하는(오름차순/내림차순) 버튼을 추가해 보려고 한다. 함수형 컴포넌트이기에 리액트 훅을 사용한다.

그리고 추가적으로 useEffect에 대해서도 설명했다. useEffect는 함수를 받는 훅이다. 이 훅은 함수 컴포넌트 안에서 함수 컴포넌트가 실행될 때마다 렌더링 끝나고 나서 자기 안의 함수를 호출해 주는 훅이다. 보통 사이드 이펙트를 넣어 주어야 할 때 사용한다. 이 훅을 통해 리턴을 함수로 해 줄 경우, 해당 컴포넌트가 사라질 때 해당 반환된 함수를 실행한다. 하지만 변수나 스코프 공간을 지우는 것은 아니다. GC는 자바스크립트 엔진이 담당한다.

useEffect(() => {
    구독();
    return () => {
        구독해제();
    };
});

 

자바스크립트에서 비동기를 처리할 때 많이 쓰는 방법이 async와 generator인데 둘 다 Promise와 연관이 있다. 그리고 둘 다 지연을 처리하는데 사용된다.

// Generator
function* foo() {}

// 비동기 함수
async function bar() {}

// 다음 두 식은 동시에 호출이 가능
// y 값이 실행될 때 x값이 확정
const x = () => 10;
const y = x() * 10;

우리가 익숙한 Promise 패턴을 통해 지연 호출이 어떠한 방식으로 일어나는지는 아래의 예제를 통해서 살펴볼 수 있다. 자바스크립트는 함수를 반환할 수 있다는 특성 때문에 지연 호출이 가능하다.

const p = new Promise(function (resolve, reject) {
    setTimeout(() => {
        resolve("1");  // 호출되면 then에서 받음
    }, 1000)
});

// 지연 호출
p.then(lfunction(r) {
    console.log(r);
});

이번에는 제너레이터에 대해 살펴본다. 제너레이터는 코루틴이라는 함수의 구현체이다. 코루틴은 리턴을 여러번 할 수 있는 함수인데, 함수가 다시 호출할 때 함수가 리턴했던 지점에서 다시 시작하는 개념을 가지고 있다. 그리고 제너레이터라는 이름은 어떤 값을 계속 만들어 내기 때문에 붙은 이름이다. 제너레이터 함수 안에서는 yield 라는 키워드를 사용하는데, 이는 일반적인 함수의 return과 비슷하다고 생각할 수 있다.

function* make() {
    let num = 1;
    while(num < 100) {
        yield num++;
    }
}

const i = make();
console.log(i); // 제네레이터 객체가 출력

이와 같이 make() 로 제너레이터를 생성하면 값이 반환되지 않고 객체가 반환이 된다. 값을 가져오기 위해서는 제너레이터 객체의 next() 메서드를 사용해야 한다. 객체의 value는 yield로 반환된 값이며 done이 true가 될 때 제너레이터는 종료된다. next()는 매개변수를 받을 수도 있다.

function* makeNumber() {
    let num = 1;

    while (true) {
        const x = yield num++;
        console.log(x);
    }
}

const g = makeNumber();

console.log(g.next()); 
// { value: 1, done: false }
console.log(g.next("a")); 
// a
// { value: 2, done: false }

기존 함수와 다르게 제너레이터는 바깥 영역과 커뮤니케이션을 할 수 있다는 특징이 있다. 바깥쪽에서는 함수 안을 제어하고 안쪽에서는 비동기를 동기적으로 실행하듯이 처리하는데 제너레이터를 이용한 대표적인 비동기 라이브러리가 redux-saga이다. async는 Promise에 최적화가 되어 있는 반면 generator는 Promise가 아닌 그냥 값들도 처리하기 좋다. 참고로 async의 내부도 제너레이터로 구현되어 있다.

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

function* main() {
    console.log("start");
    const result = yield delay(3000);
    cosnole.log("3초 후");
}

const it = main();
const { value } = it.next(); // 이 때 promise객체가 온다.

if (value instanceof Promise) {
    value.then(() => it.next());
}


// 참고로 async 에서는 이렇게 한다
async function main2() {
    console.log("start");
    await delay(3000);
    console.log("3초 뒤");
}

제너레이터에 대해서는 이전에 좀 더 자세하게 포스팅 해놓은 글이 있으므로 참고하기 바란다.

 

살짝 늦은 감이 있지만.. 2주차 우아한 테크 러닝 복습도 마무리!