우아한 테크러닝 3기

✔ 우아한 테크러닝 3기: React & TypeScript 3회차

seungmin2 2020. 9. 9. 22:52

✌ 9월08일 (화) 우아한 테크러닝 3기 3회차 강의 정리

🚀 3회차 강의 목표

  • 리액트 만들어보기
  • 리액트의 기본적인 부분들과 리액트를 구현해보자! 😤

    📌 리액트를 구현해보는 이유!
    개발자가 사용자입장에서만 사용하지 않았으면 좋겠다.
    이렇게 구현해보면서 더 좋은 개발자가 되었으면 좋겠다.

🚀 React를 만들기 전에..

  • list와 같은 데이터를 기반으로 JavaScript를 사용할 땐 아래와 같이 rootElement.innerHTML사용하여 화면에 표시할 수 있다.

    // index.js
    const list = [
      { title: "React에 대해 알아봅시다." },
      { title: "Redux에 대해 알아봅시다." },
      { title: "TypeScript에 대해 알아봅시다." }
    ];
    const rootElement = document.getElementById("root");
    function app() {
      rootElement.innerHTML = `
          <ul>
                ${list.map((item) => `<li>${item.title}</li>`).join("")}
          </ul>
      `;
    }
    app();
  • 위의 app()함수는 list를 바깥에서 직접 가져다 쓰기 때문에 좋지 못하다.

  • 바깥의 영향을 안 받게 순수 함수로 만들어 준다.

    // 생략..
    function app(items) {
      rootElement.innerHTML = `
          <ul>
                ${items.map((item) => `<li>${item.title}</li>`).join("")}
          </ul>
      `;
    }
    app(list);

    🌈 React가 탄생한 이유

  • 위와 같이 rootElement.innerHTML의 DOM을 직접 사용하여 작성한다.

  • 이럴때 데이터가 많아지고 UI도 많아지고, UI가 많아지다보니 어떨땐 A라는 UI를 사용자에게 보여주고, 어떨땐 B라는 UI를 사용자에게 보여주고, C라는 이미지를 보여주게 된다. 이런것을 라우팅이라고 한다.

  • 데이터도 위와 같이 코드에 박혀 있는 것이 아니고 서버에 있다보니 API의 호출을 통해서 비동기적으로 필요한 상황에 적시적소 원하는 타이밍에 데이터를 가져온다. 이렇듯 끊임없이 변화한다.

  • 이렇게 변하다보니 한달 뒤에 또는 두달 세달 뒤에 코드를 고칠려고 할 때 생각이 잘 안난다...

  • 빨리 생각이 날 수 있는 구조가 있는데 여기에 큰 원칙으로 좋은 아키텍처는 같은 것끼리 묶어주고, 다른 것끼리 분리하는 것이다.(대원칙)

  • 이 원칙의 가장 중요한 첫 번째 할 수 있는 일은 네이밍을 잘 지어주는 것으로 네이밍만 잘해줘도 70%는 해결된다.

  • 위 코드처럼 Real DOM을 가지고 HTML UI를 조작하는 것은 안전성이 매우 떨어진다.

  • 그 이유는 Real DOM은 너무 Low Level API이기 때문이여서 추상화 수준이 높지 못하다.

  • 이것으로 조작하다보면 필연적으로 복잡도가 올라가게 되는 것이다. 그 말은 즉, 수정하기가 매우 까다롭다.

  • 이런 상황을 해결하기 위해 react와 같은 것들이 나오게 되었다!🤩

  • 브라우저는 HTML같은 것을 DOM이라는 트리 구조의 데이터로 변환해서 관리한다.

  • 이처럼 자바스크립트 자체로 직접 접근하는 것 또한 까다로우니까 Virtual DOM을 사용한 React가 나왔다.

  • 사실상 HTML 문자열을 브라우저가 DOM이라는 트리 구조의 객체 형태로 만드는 것이나 DOM을 Virtual DOM으로 만드는 것이다 결국 똑같은 컨셉이다.

🚀 React의 기본 이해와 Virtual DOM

  • 기본 구조

    import React from "react";
    import ReactDOM from "react-dom";
    function App() {
      return (
          <div>
              <h1>Hello?</h1>
              <ul>
                  <li>React</li>
                  <li>Redux</li>
                  <li>MobX</li>
                  <li>Typescript</li>
              </ul>
          </div>
      );
    }
    ReactDOM.render(<App />, document.getElementById("root"));
  • JSX문법이며 Babel이 변환해준다.

  • Babel의 react plugin이 리액트 테그를 변환해주는 것이다. 그렇기 때문에 실제로는 결국 그냥 함수 호출에 불과하다.

  • ReactDOMrender 정적 메소드를 갖는데 2개의 인자를 받는다.

  • 첫 번째 인자는 화면에 렌더링할 컴포넌트이며 두 번째 인자는 컴포넌트를 렌더링할 HTML 요소이다.

    // Babel에서 jsx를 javasript로 변환
    "use strict";
    var _react = _interopRequireDefault(require("react"));
    var _reactDom = _interopRequireDefault(require("react-dom"));
    function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
    function App() {
        return /*#__PURE__*/_react.default.createElement("div", null, /*#__PURE__*/_react.default.createElement("h1", null, "Hello?"), /*#__PURE__*/_react.default.createElement("ul", null, /*#__PURE__*/_react.default.createElement("li", null, "React"), /*#__PURE__*/_react.default.createElement("li", null, "Redux"), /*#__PURE__*/_react.default.createElement("li", null, "MobX"), /*#__PURE__*/_react.default.createElement("li", null, "Typescript")));
    }
    _reactDom.default.render( /*#__PURE__*/_react.default.createElement(App, null), document.getElementById("root"));

    react의 createElement는 Virtual DOM을 만들게 된다.
    자바스크립트 사이드에서는 createElement라는 함수를 이용해서 Virtual DOM이 만들어지고 리액트는 자신이 만든 것이라 잘 알기 때문에 Virtual DOM을 만든 다음에 그것을 그대로 Real DOM으로 변환해주는 것이다.
    그리고 개발자들에게는 createElement를 안쓰기때문에 컴포넌트로 좀 더 쉽게 사용하기 위해서 JSX를 제공하는 것이다.

    import React from "react";
    import ReactDOM from "react-dom";
    // StudyList 이름을 가질 수 있다.
    function StudyList() {
      return (
          <ul>
              <li>React</li>
              <li>Redux</li>
              <li>MobX</li>
              <li>Typescript</li>
          </ul>
      );
    }
    function App() {
      return (
          <div>
              <h1>Hello?</h1>
              <StudyList />
          </div>
      );
    }
    ReactDOM.render(<App />, document.getElementById("root"));
  • 위와 같이 분리를 해서 컴포넌트 이름을 지정해서 사용할 수 있다.

  • 이렇게 이름을 줄 수 있는 구조는 더 이상 MarkUp을 보고서는 우리가 어떤 데이터가 어디에 두었는지를 고민하지 않고 컴포턴트 이름만(잘 지어졌을 경우) 보고서 확인이 가능함으로써 읽기 쉬워지고, 읽기 쉬어짐에 따라 고치기도 쉬워진다.

  • 위의 StudyList 함수에서 반환하는 JSX 엘리먼트들을 Babel로 변환되면 아래와 같은 형태이다.

    function StudyList() {
      return /*#__PURE__*/_react.default.createElement("ul", null, /*#__PURE__*/_react.default.createElement("li", {
        className: "item"
      }, "React"), /*#__PURE__*/_react.default.createElement("li", {
        className: "item"
      }, "Redux"), /*#__PURE__*/_react.default.createElement("li", {
        className: "item"
      }, "MobX"), /*#__PURE__*/_react.default.createElement("li", {
        className: "item"
      }, "Typescript"));
    }
  • 변환된 상태를 참고하여 객체로 바꾸어보면 아래의 vdom과 같은 형태이다.

    const vdom = {
      type: "ul",
      props: {},
      children: [
          { type: "li", props: { className: "item" }, children: "React" },
          { type: "li", props: { className: "item" }, children: "Redux" },
          { type: "li", props: { className: "item" }, children: "Typescript" },
          { type: "li", props: { className: "item" }, children: "MobX" },
      ],
    };
  • 이렇게 확인해보면 왜 React가 결국 맨 상위 단 하나의 최상위 부모 컨포넌트가 있을 수 밖에 없는지 알 수 있는 구조이다.

  • 이렇듯 react는 Virtual DOM이 단 하나인 이유이다.

🚀 React 만들어보기!

  • 위의 JSX가 변환된 모습을 보면 createElement는 아래와 같은 형태로 이루어져있다.

    // props는 없으면 빈 객체, children는 여러 개의 배열
    function createElement(type, props = {}, ...children) {
    return { type, props, children };
    }
  • 위에서 만들어본 vdomcreateElement를 이용하여 만들 수 있다.

    // 예시
    const vdom = createElement("ul", {}, createElement("li", {}, "React"));

  • createElement는 vdom 객체를 만드는 함수고 render할 때, render 객체는 계속 쫒아가면서 realdom으로 변환한다.

  • 최종적으로 다 만들면 root 컴포넌트 컨테이너한테 append child를 하는 방식이다.

  • 바벨의 옵션을 이용해서 맨 상단에 주석을 넣어준다.

  • @jsx의 지시어를 이용해서 내가 원하는 이름을 주면 createElement가 원하는 이름으로 변경이 된다.

    /* @jsx createElement */
    function StudyList(props) {
      return (
          <ul>
              <li className="item">React</li>
              <li className="item">Redux</li>
              <li className="item">MobX</li>
              <li className="item">Typescript</li>
          </ul>
      );
    }
  • 이 말을 즉, 바벨만 가지고 Virtual DOM을 만들 메소드(트랜스파일될 메소드)를 임의로 바꿔서 내가 만든 다른 것으로 교체가 가능하다.

  • 이 과정은 컴파일 타임이다.

    // createElement의 이름으로 변환되었다.
    "use strict";
    /* @jsx createElement */
    function StudyList(props) {
    return createElement("ul", null, createElement("li", {
      className: "item"
    }, "React"), createElement("li", {
      className: "item"
    }, "Redux"), createElement("li", {
      className: "item"
    }, "MobX"), createElement("li", {
      className: "item"
    }, "Typescript"));
    }
  • @jsx지시어를 사용하면 js문법을 찾아서 자동으로 변환을 해준다.

  • 이렇게 createElement로 변하게 되고 우리가 만든 createElement함수로 들어가게 된다.

    /* @jsx createElement */
    function createElement(type, props = {}, ...children) {
    return { type, props, children };
    }
    function StudyList(props) {
    return (
      <ul>
        <li className="item" label="haha"> React </li>
        <li className="item">Redux</li>
        <li className="item">TypeScript</li>
        <li className="item">Mobx</li>
      </ul>
    );
    }
    function App() {
    return (
      <div>
        <h1>Hello?</h1>
        <StudyList item="abcd" id="hoho" />
      </div>
    );
    }
    //  변환되면 console.log(createElement(App, null));
    console.log(<App />); 
  • 이렇게 console.log(<App />); 해보게 되면 아래 사진처럼 App함수가 들어간다.

  • type의 타입을 비교하는 로직을 추가해준다.

    function createElement(type, props = {}, ...children) {
    if (typeof type === "function") {
        // 호출해준다.
      return type.apply(null, [props, ...children]);
    }
    return { type, props, children };
    }

🌈 리액트에서 사용자 컴포넌트가 대문자로 시작하는 이유

  • 변환된 App안의 태그들을 확인해보면 문자열로 들어가는 것을 확인할 수 있다.
    function app() {
    return (
      <div>
        <h1>Hello?</h1>
        <StudyList item="abcd" id="hoho" />
      </div>
    );
    }
    // 변환
    function app() {
    return createElement("div", null, createElement("h1", null, "Hello?"), createElement(StudyList, {
      item: "abcd",
      id: "hoho"
    }));
    }
  • 하지만 대문자로 시작한 App은 함수 그 자체로 넣어버린다. (Type이 function)
    // 대문자일 때
    console.log(<App />);
    // 변환
    console.log(createElement(App, null));
    // 소문자일 때
    console.log(<app />);
    // 변환
    console.log(createElement("app", null));
  • 이처럼 리액트에서는 사용자 컴포넌트는 무조건 대문자로 시작해서 사용해야 한다.
  • createElement로 생성된 객체를 실제 DOM으로 구성하는 render 함수를 작성한다.

  • createElement로 생성된 객체를 보면 같은 형태로 계속 이루어져있다.

  • 그리고 마지막에는 전부 appendChild를 해준다.

    // 재귀함수
    function renderElement(node) {
    if (typeof node === "string") {
      return document.createTextNode(node);
    }
    const el = document.createElement(node.type);
    node.children.map(renderElement).forEach((element) => {
      el.appendChild(element);
    });
    return el;
    }
    function render(vdom, container) {
    container.appendChild(renderElement(vdom));
    }
  • props를 넘겨받는 컴포넌트의 경우에도 같은 형식으로 적용할 수 있다.

    function Row(props) {
    return <li>{props.label}</li>;
    }
    function StudyList(props) {
    return (
      <ul>
        <Row label="하하하" />
        <li className="item" label="haha">React</li>
        <li className="item">Redux</li>
        <li className="item">TypeScript</li>
        <li className="item">Mobx</li>
      </ul>
    );
    }
  • render 함수를 사용하여 real DOM의 HTML에 붙여 넣을 수 있다.

    render(<App />, document.getElementById("root"));

📌 전체 소스는 GitHub 확인해주세요! 🙏

🚀 React의 클래스 컴포넌트와 함수 컴포넌트

🌈 클래스 컴포넌트

  • 클래스 컴포넌트는 라이프사이클 API를 사용하여 state를 생성자에서 초기화할 수 있다.
    class Hello extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        count: 1
      };
    }
    componentDidMount() {
      this.setState({ count: this.state.count + 1 });
    }
    render() {
      return <p>안녕하세요!</p>;
    }
    }
  • react 공식문서 확인하기 ❗❗

🌈 함수 컴포넌트(Hook)

  • 함수 컴포넌트는 초창기에는 상태는 함수가 호출될 때 마다 생성되기 때문에 유지될 수 없었기 때문에 상태를 관리하지 못한다고 여겨져 사용하지 않았다.
  • 아래처럼 안에 변수는 함수 내부의 상태처럼 여겨졌다.
    function App() {
      let x = 10;
      return (
          <div>
              <h1>상태</h1>
              <Hello />
          </div>
      );
    }
  • 하지만 함수 컴포넌트도 Hooks가 나온 뒤 상태 관리를 할 수 있게 되었다.
    function App() {
    // 반환하는 값은 배열
    const [counter, setCounter] = useState(1);
    // const counter = result[0];
    // const setCounter = result[2];
    return (
      <div>
        <h1 onClick={() => setCounter(counter + 1)}>상태{counter}</h1>
        <Hello />
      </div>
    );
    }
  • h1 태그를 클릭하면 counter 상태가 1씩 증가한다.
  • hook은 항상 최상위(at the Top Level)에서만 Hook을 호출해야 해야한다. react Hooks 규칙 참고
  • 최상위가 아닌 부분에서 호출 될 경우 전역 배열에 원하지 않는 값을 반환하여 문제가 발생 할 경우가 생길 수 있다.
  • 공식 문서를 참고하자! 😤

🚀 우아한 테크러닝 3기 3회차 후기

이번 시간은 리액트를 직접 만들어보았다. 😀
만들어보면서 계속 파보면서 여태까지 아무것도 모르고 리액트를 사용했다고 생각했다..
이번 계기로 왜? 라는 질문을 계속 스스로에게 해야할 거 같다.
그냥 표면적으로 보이는 것만 보지 말고 원리를 이해하도록 노력해야 할 거 같다.
3회차까지 들으면서 김민태 강사님의 열정과 정성이 느껴져서 너무 좋았다.(2회차는 10시 반까지.. 3회차도 7시부터 QnA 시간..)
내일이면 반을 했는데 끝나면 아쉬울거 같다. 열심히 듣자.😤