우아한 테크러닝 3기

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

seungmin2 2020. 9. 11. 23:26

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

🚀 4회차 강의 목표

  • 💻 React 컴포넌트의 상태와 관리
  • 🎯 커뮤니케이션!!
  • 😤 비동기와 제너레이터
    • 타입스크립트는 다음주부터! 🤩

🚀 React 컴포넌트의 상태와 관리

React 애플리케이션을 만든다.

  • index.js
    import React from "react";
    import ReactDOM from "react-dom";
    ReactDOM.render(
      <React.StrictMode>
          <App />
      </React.StrictMode>,
      document.getElementById("root")
    );
  • App.js
    import React from "react";
    const App = () => {
      return (
          <div>
              <header>
                  <h1>React and TypeScript</h1>
              </header>
              <ul>
                  <li>1회차: Overview</li>
                  <li>2회차: Redux 만들기</li>
                  <li>3회차: React 만들기</li>
                  <li>4회차: 컴포넌트 디자인 및 비동기</li>
              </ul>
          </div>
      );
    };
    export default App;

위와 같은 형태를 되어있는 것을 li 태그(상태 값) 데이터를 객체로 만들어 index.js에서 App컴포넌트로 값을 넘겨준다.(아래)

  • index.js
    import React from "react";
    import ReactDOM from "react-dom";
    import App from "./App";   
    const rootElement = document.getElementById("root");
    const sessionList = [
      { title: "1회차: Overview" },
      { title: "2회차: Redux 만들기" },
      { title: "3회차: React 만들기" },
      { title: "4회차: 컴포넌트 디자인 및 비동기" }
    ];
    ReactDOM.render(
      <React.StrictMode>
        <App store={{ sessionList }} />
      </React.StrictMode>,
      rootElement
    );
  • App.js
    import React from "react";
    const App = (props) => {
      const {sessionList} = props.store;
      return (
          <div>
              <header>
                  <h1>React and TypeScript</h1>
              </header>
              <ul>
                 {sessionList.map((session) => (
                      <li>{session.title}</li>
                  ))}
              </ul>
          </div>
      );
    };
    export default App;

이렇게 li태그를 map을 사용하여 생성할 수 있다.

  • 하지만 이 코드는 좋지 못하다.

    모든 리액트 컴포넌트 안에 리턴되는 JSX들이 있는데 코드가 코드로써 렌더링되는 코드들이 많아지면 readability(가독성) 이 떨어진다. 그렇기 때문에 컴포넌트를 한번 더 감싸서 만드는 것이 좋다.

  • App.js

    import React from "react";
    // li 태그 분리
    const SessionItem = ({ title }) => <li>{title}</li>;
    // App 컴포넌트
    const App = (props) => {
      const {sessionList} = props.store;
      return (
          <div>
              <header>
                  <h1>React and TypeScript</h1>
              </header>
              <ul>
                 {sessionList.map((session) => (
                      <SessionItem title={session.title} />
                  ))}
              </ul>
          </div>
      );
    };
    export default App;

    이렇게 분리를 하면 훨씬 더 좋은 가독성이 생긴다.
    하지만 지금의 컴포넌트는 상태를 가지고 있지 않는 컴포넌트이다.

    버튼(토글형식)을 누르면 오름차순, 내림차순으로 바뀌는 상태를 가진 컴포넌트를 만들어보자!

  • App.js

    import React from "react";
    const App = (props) => {
      let displayOrder = "ASC";
      const { sessionList } = props.store;
      // order라는 이름으로 인덱스를 넣어준다.
      const orderedSessionList = sessionList.map((session, i) => ({
          ...session,
          order: i,
      }));
      const toggleDisplayOrder = () => {
          displayOrder = displayOrder === "ASC" ? "DESC" : "ASC";
      };
    
      return (
          <div>
              <header>
                  <h1>React and TypeScript</h1>
              </header>
              <button onClick={toggleDisplayOrder}>재정렬</button>
              <ul>
                  {orderedSessionList.map((session) => (
                      <SessionItem title={session.title} key={session.id}/>
                  ))}
              </ul>
          </div>
      );
    };
    export default App;
  • 하지만 위와 같은 상태에서는 버튼을 클릭해봤자 displayOrder의 값은 변경되지 않고 다시 렌더링되지도 않는다.

🌈 함수형 컴포넌트 왜 렌더링되지 않았을까?

  • 위 상황에서 함수 컴포넌트는 onClick={toggleDisplayOrder}를 넣어서 버튼을 클릭하고 값을 바꿔봐도 갱신이 되지않는다.
    리액트 입장에서 이 App이 다시 호출되서 Virtual DOM이 다시 만들어지고 다시 DOM에다가 다시 업데이트를 해야하는데 안에서 다시 호출한다는 무언가의 신호(signal)을 함수 바깥으로 보낼 수가 없다. 왜냐하면 함수는 이미 끝났기 때문이다.

  • 그래서 초창기에는 클래스 컴포넌트를 사용할 수 밖에 없었던 것이다.

🌈 클래스 컴포넌트는 어떻게 상태가 바뀔까?

함수형 컴포넌트를 클래스 컴포넌트로 간단하게 바꿔본것이다.

  • App.js
    class ClassApp extends React.Component {
    constructor(props) {
      // this.props 컨텍스트 객체
      super(props); // 컨벤션
      // this.onToggleDisplayOrder = this.onToggleDisplayOrder.bind(this);
      this.state = {
        displayOrder: "ASC"
      };
    }
    // onToggleDisplayOrder(){
    //   // 그냥 함수 실행 컨텍스트를 따른다.
    //   this.setState({
    //     displayOrder:displayOrder ==='ASC' ? 'DESC' : 'ASC'
    //   })
    // }
    toggleDisplayOrder = () => {
      this.setState({
        displayOrder: displayOrder === "ASC" ? "DESC" : "ASC"
      });
    };
    render() {
      return (
        <div>
          여기여기
          <button onClick={this.onToggleDisplayOrder}>정렬</button>
        </div>
      );
    }
    }
  • 여기서 superextends뒤에 있는 것이고 prototype chain의 상위 부모이기 때문에 super한테 그대로 넘겨주면 리액트가 props에다가 넣어준다. (컨텍스트 객체)
  • 기본값을 this.state안에다가 넣어준다.
  • 여기서 일반함수 onToggleDisplayOrder는 실행 컨텍스트를 따라서 this가 바뀔 수 있는데, arrow 함수는 렉시컬 컨텍스트(문맥 컨텍스트)를 따르기 때문에 지금 컴파일 타임의 이 순간(코드흐름)대로 따라가게 된다. 그래서 바인딩도 필요가 없게 된다.

🌈 그래서 중요한건 어떻게 상태를 가질 수 있을까?

  • 이런 과정의 이후에는 이 컨텍스트 객체만 가지고 만들어진 인스턴스 객체를 가지고 리액트를 핸들링하기 떄문에 없어지지 않는다. 즉, 다시 호출된다는 개념이 아니라 이미 존재하는 객체가 업데이트 되었으면 그 객체 안에 render메소드를 호출하는 개념이다.
  • 그래서 this를 다시 만드는 것이 아니기 때문에 클래스 컴포넌트가 상태를 가질 수 있는 것이다.

🌈 함수형 컴포넌트 어떻게 상태를 가질까?

  • 애초에 함수형 컴포넌트는 상태를 갖는건 불가능하다.
  • 하지만 react 팀에서 그 함수의 호출과정 언제 호출되는지, 순서등과 같은 것들을 모두 다 제어하고 있기 때문에 그거에 맞춰서 각자의 함수에 상태를 저장하는 것을 제공해주면 함수도 상태를 가질 수 있다라고 나온 것이 react hooks 이다.
  • 결국 클래스 컴포넌트의 render가 다시 불러오는 것과 같은 맥락이다.

🌈 함수형 컴포넌트를 선호하는 이유

  • App클래스 컴포넌트만 봐도 상태가 등장하는 것에 여러 메소드들이 분산이 되어있다.
  • 함수형 컴포넌트는 이와 다른데 함수하나에 모두 몰려있는 것을 볼 수가 있는데 이런 것을 응집성이라고 하는데 모여있으면 훨씬 보기에 좋고 여러가지 측면에서 장점이 있다고 얘기를 한다. 또한 선언적이다.

🌈 함수형 컴포넌트의 Hook을 사용한 마무리

  • App.js

    import React from "react";
    const SessionItem = ({ title }) => <li>{title}</li>;
    const App = (props) => {
    const [displayOrder, toggleDisplayOrder] = React.useState("ASC");
    const { sessionList } = props.store;
    const orderedSessionList = sessionList.map((session, i) => ({
      ...session,
      order: i
    }));
    
    const onToggleDisplayOrder = () => {
      toggleDisplayOrder(displayOrder === "ASC" ? "DESC" : "ASC");
    };
    
    return (
      <div>
        <header>
          <h1>React and TypeScript</h1>
        </header>
        <p>전체 세션 갯수: 4개 {displayOrder}</p>
        <button onClick={onToggleDisplayOrder}>재정렬</button>
        <ul>
          {orderedSessionList.map((session) => (
            <SessionItem title={session.title} key={session.id} />
          ))}
        </ul>
      </div>
    );
    };
    export default App;

    이렇게 useState Hook을 사용하여 상태를 관리할 수 있게 되었다.
    전체 소스 코드는 GitHub 참고

🌈 함수형 컴포넌트의 Hooks는 클로저이다?? 🤔

  • 실제로 Hooks는 클로저가 아닌거 같다! 🙄 하지만 Hooks를 사용할땐 당연히 클로저가 전달된다!
  • 위의 displayOrder의 값의 상태를 변경할려면 당연히 toggleDisplayOrder함수를 호출해야 되고, 이 함수는 반드시 다른 함수 안에 콜백으로 들어갈 수 밖에 없는 구조이다. 😮 이 상황에서 클로저가 잡하는 것이다.
  • 왜나하면 toggleDisplayOrder 함수가 호출되는 시점에 호출되면 안되는 것은 호출을 하면 다시 render가 될 것이고 그렇게 render가 되고 다시 toggleDisplayOrder 함수가 호출되기 때문에 무한루프에 빠지게 되는 것이다.

    그렇기 때문에 Hook 자체가 클로저가 아니라 함수안에서 클로저가 잡히는 것이다! 🚀

🌈 클로저 같은 것들이 메모리가 만들어졌으면 이걸 언제 해제하거나 제거할 수 있을까?? (메모리 할당/해제)

  • 아래의 예를 보자.

    // ex)
    useEffect(() => {
      document.title = '';
    })

    위를 확인해보면 document.title은 리액트와 무관한 바깥에서 일어나는 일이다.
    이런 것들을 side-effect라고 하는데 제어가 불가능하고 부수효과를 이르킬수 있는 요소들을 말하는 것이다.

  • useEffect안에서 return을 함수로 해주면 실제로 UI에서 사라질 때 이 함수를 호출을 해주는데 정리할 리소스들을 해제할 것들을 하라고 나와있다. (공식문서 참조)

    // ex)
    useEffect(() => {
      // ...
      return () => {
    
      }
    })
  • 하지만 위에서 return을 함수로 호출하는 방법은 정리 대상의 요소들은 실질적인 대상에 불과하고 메모리, 변수, 클로저 공간, 스코프 공간 이런 것들은 대상이 아니다.

  • JavaScript 에서 변수안에 있는 데이터 혹은 객체를 임의로 지우는 방법은 존재하지 않는다. (언어적으로 제공하지 않는다.)
    하지만, 무한히 생성되는 것은 아니고 GC(가비지 컬렉션)이라는 메커니즘이 존재하는데 사용하지 않는 것들은 수집해서 자바스크립트가 Mark-and-sweep이라는 알고리즘을 사용하여 제거를 한다.

  • 그때 컴포넌트가 사라지면 같이 그 안에 존재하던 일반 객체 뿐만 아니라 클로저와 같은 스코프 공간들도 참조가 없어지면 GC가 일어나는 것이다.

  • 자바스크립트에서 그런 종류의 데이터 스토리지들을 명시적으로 제거하는 방법은 제공하지 않기 때문에 react에서 clean up 대상은 클로저 같은 것이 아니기 때문에 굳이 메모리에 대해서 신경을 쓰지 않아도 된다. (알아서 해준다.)

🚀 커뮤니케이션에 대해서..

  • 커뮤니케이션은 일종의 프로토콜(정보를 주고 받기 위한 약속)과 같다.
  • 이렇듯 일하면서나 면접에서 봐도 이 프로토콜이 안 맞는 경우가 굉장히 많다. 그리고 더욱 더 큰 문제는 프로토콜이 안 맞는다라는 것을 의식조차 못하고 있는 사람들도 꽤 많다는 것이다.
  • 그렇기에 프로토콜이라는 것은 정보를 주고 받는다는 건(이해를 한다는 건) 어느 정도의 맥락(수준)이 일치 되야 한다는 것이다.
    이 수준의 layer라고 생각을 하면 어떤 A를 상대방에게 전달할 때 상대방이 이해할 수 있는 layer를 파악하고 있는 것이 굉장히 중요하다. 즉 이말은 개발자가 개발자가 아닌 사람한테 개발용어를 설명한다는 것과 비슷한 것이다.
    그런 부분을 잘 알고 있어야하고 일방적인 소통을 하면 안된다는 점이다.
  • 학습하는 부분에 있어서도 같은 맥락이라 볼 수 있다.
  • 학습도 나와의 커뮤니케이션이라는 것으로 그 지식이 나와 맞으면 그 지식이 잘 들어온다. 하지만 맞지 않는다면 그 원하고자 하는 지식은 눈에 들어오지 않는 것이다.
  • 그렇기 때문에 어느 layer의 지식이 나와의 프로토콜에 맞을까를 생각해서 스스로 조정이 필요하다.😤
  • 또한 어느 지식에 대해서 그 하나만 몰입하지 않았으면 좋겠고 너무 고급기술만 배울려고 하지 않았으면 좋겠다.

    🙏 신입 개발자 또는 주니어 개발들에게..
    신입분들은 대부분 의욕이 넘쳐나고 충만한 상태인데 그 의욕은 굉장히 좋지만 반대로 매우 위험한 상태일 수도 있다.
    어느 한쪽에 몰입하여 잘 못 빠지게 되면 훅 들어가 버릴 수가 있다.
    그렇기 때문에 항상 자기를 돌아봐보는 시간을 갖는 것이 좋을 거 같다.🌈

🚀 비동기와 제너레이터

🌈 제너레이터

  • 제너레이터는 function * 으로 사용된다.

    function* foo() {}
  • 아래 코드를 확인해보자.
    x값이 확정짓기 전에 계산을 수행할 수 없기 때문에 순차적으로 실행될 수 밖에 없다.
    즉, y의 변수에 x의 의존성이 있기 때문에 x가 없이 y가 생길 수 없게 되는 것이다.

    const x = 10;
    const y = x*10;
  • 함수인 경우는??
    함수는 호출을 하면 그냥 함수만 담고 실제로 x값을 확정하는 순간을 지연시킨다.(지연호출)

    const x = () => 10;
    const y = x*10;
  • Promise도 이 지연과 굉장히 비슷하다.

    const p = new Promise(function (resolve, reject) {
    // 함수로 실행해서 지연을 일으킨다.
      setTimeout(() => {
        resolve("1");
      }, 100);
    });
  • 제너레이터는 제너레이터 객체를 반환하는데 제너레이터 객체는 코루틴이라는 함수의 구현체다.

    • 코루틴이란??🤔
      함수는 인자를 받고 무언가 계산을 하고 리턴을 한다. 그리고 한 번 호출하고 결과를 받거나 그 다음 스텝을 실행한다.
      이런 상황을 폭 넓게 넓혀 호출자한테 리턴을 여러 번 할 수 있게 해주는 것이다.
      리턴을 여러 번 한다는 것은 다시 함수가 호출될 때 처음부터 시작되는 것이 아니라 마지막 리턴한 지점부터 다시 시작한다는 컨셉이고 함수를 조금 더 일반화한 컨셉으로 확장해서 기능을 추가를 해 놓은 것이 코루틴이다.
      자바스크립트에서는 그 코루틴의 개념을 일부 차용해서 제너레이터라고 명명하였다.
  • 일반적인 함수인 경우는 make함수의 반환 값을 1이 될 것이다.

    function make(){
      return 1;
    } // 1
    // 제너레이터
    function* make() {
      return 1;
    }
    console.log(make());
  • 아래와 같이 function*를 사용하면 이 제너레이터 객체를 반환한다.

  • 제너레이터는 yield과 같이 사용한다.

    function* makeNumber() {
    let num = 1;
    // 무한 루프이지만 문제가 없다.
    while (true) {
      // 제너레이터 안의 리턴이지만 함수를 끝내지 않고 바깥으로 내보낸다.
      // 기존 상태를 저장하고 있다.
      yield num++;
    }
    }
  • 아래에 makeNumber() 제너레이터를 생성해도 값이 반환되지 않고 객체를 넘겨준다.

    const j = makeNumber();
  • 위의 makeNumber를 호출하면 제너레이터 객체를 넘겨주고 여기에는 next라는 메소드가 있다.

    console.log(j.next());
  • 콘솔에 찍힌 값을 확인해보면 valueyield로 반환된 값이며 donetrue일 경우 제너레이터가 종료된다.
    즉, yieldreturn이 나올 때 까지 실행하는 것이다.

  • 제너레이터는 next는 값을 넣을 수도 있다.

    function* makeNumber() {
      let num = 1;
      while (true) {
        const x = yield num++;
        console.log(x);
      }
    }
    const j = makeNumber();
    console.log(j.next());
    console.log(j.next("a"));
  • 아래 사진과 같이 next에 매개변수로 넘겨준 문자열 ayield의 반환값으로 담겨서 사용할 수도 있는 것을 확인할 수 있다.

  • 제너레이터를 활용해서 비동기함수를 동기적으로 나타낼 수 있다.

    const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
    // 동기 함수처럼 보인다.
    // 바깥쪽에서 상황을 해결한다.
    function* main() {
      console.log("시작");
      yield delay(3000);
      console.log("3초뒤");
    }
    const it = main();
    const { value } = it.next();
    console.log(value);
  • 위의 코드를 실행하면 시작이 출력되고 3초 뒤에 3초 뒤라고 출력된다.

  • async/await를 사용해서 비슷하게 사용할 수 있다.

    // async
    async function main2() {
    console.log("시작");
    await delay(3000);
    console.log("3초뒤");
    }
    main2();
  • async/awaitPromise에 최적화 되어있다.

  • 그렇지만 제너레이터를 사용할 경우 Promise 형태의 값이 반환되지 않아도 사용이 가능하기 때문에 더 다양한 상황에서 응용이 가능하다.

  • 전체 소스는 GitHub를 참고 🙏

🚀 4회차 후기

오늘도 역시나 역시 배운게 많았다. 생소한 용어도 많이 있었다. 찾아서 공부를 더 해봐야겠다.😤
다음 주부터는 typescript에 대해서 배운다는데 집중해서 들어야겠다..📖
벌써 반이나 와버렸다.. 시간이 너무 빠르다. 블로깅하는 것이 쉽지않은데 끝까지 아자아자! 해야겠다!!!✨