우아한 테크러닝 3기(3) - JS로 리액트 구현하기

둘둘·2020년 9월 9일
6

우아한_테크러닝

목록 보기
3/3
post-thumbnail

지난 시간에 리덕스를 자바스크립트로 구현했던 것처럼
이번 시간에는 리액트를 자바스크립트로 구현하는 것이 강의 목표!

  • 리액트에 대한 기본적인 부분
  • 리액트 구현해보기

리액트

(도입부터 띵언으로 스타트!)
공식문서 보고 학습하다 보면 지식의 넓이가 넓어질 수 있다.
하지만, 깊이보다 넓이를 추구하다 보니 자꾸만 이것저것 학습하려고 하고 그러다 보면 쉽게 지친다.


지금 나의 상태가 약간 그런거 같은데.. 어떻게 이렇게 잘 아시죠 ㅠㅠ

어찌됐든 우리는 개발 소비자로 남는것보다 라이브러리 하나를 공부하더라도 어떤 원리로 작동이 되고 있는지 깊이 파봐야 10년 뒤의 연봉이 달라진다고 하셨다 크으~

모든 라이브러리나 프레임워크의 사용법은 공식문서 보면 참 잘 나와있다.
우리가 공부하면서 파악해야 하는 것은 why.
하지만 왜 그렇게 사용해야 하는가에 대한 내용은 공식문서에 나오지 않는다.

그래서 리액트의 사상을 이해하는 데 문제가 없을만한 핵심적인 부분을 짚고 넘어가는 것이 우리의 목표!

시작

리액트를 조금이라도 공부해봤던 사람이라면
index.html 파일 안에 아래의 id가 root인div가 존재한다는 사실을 알 것이다.

<div id="root" /> 

아래의 list라는 데이터를 이제 ui로 그려보자.

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();

코드펜에 복붙했을 때 바로 좌측과 같은 목록이 만들어진다.
여기서 잠깐..

🙋🏻‍♀️ 리빙포인트!!
아키텍처적으로 가장 쉽게 할 수있는 일 : 이름 잘 짓기
또 다른 중요한 포인트 : 같은 것끼리 묶고 다른 것들읕 분리하자

나머지 아키텍처적인 원칙들은 20%를 차지할 수있음. 이름짓기가 팔할이다!
너무 높은 수준의 무언가를 추구하다가 기본적인 것을 소홀히 하는 경우가 많다
그래서 이름 짓는게 참 중요하다.

또, 한번만 만들어놓고 절대 변하지 않는 앱이라면 코드에 대한 공수가 많이 들지 않는다. 하지만 문제는 끊임없이 변한다는 점이다.
한 달전에 만들었던걸 한달 뒤에 고치려고 하면 생각이 안남. 왜냐. 까먹어서!
코드를 짜다보면 어떤 것이 좋은 아키텍처인가에 대한 고민을 하게 되는데,
결국 같은 것끼리 묶고 다른 것들은 분리하자는 결론이 도출된다.

(다시 본론으로 돌아와서..)

위의 코드에서 app 함수의 모양새를 약간 바꿔줘야 한다.
현재 상태론 바깥쪽 dependency에 영향을 받기 때문에!

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

app(list);

짠~ 순수 함수로 만들어주었음

🙋🏻‍♀️ 다시 돌아온 리빙포인트!!
추상화레벨이 낮다 👉 복잡도가 올라간다 👉 수정하기가 힘들어진다.

리얼돔의 api가 너무 low레벨(추상화수준이 낮음)이라 그걸로 앱의 ui를 다루다 보면 필연적으로 복잡도가 올라가게 된다. 그 말인 즉슨, 수정하기가 힘들어진다는 것이다.
리얼돔을 직접적으로 다루는 라이브러리들도 나중에 복잡도가 높아지는걸 피할수없음
ex) 제이쿼리

제이쿼리 자체가 나쁘다기 보단 우리가 다룰 앱이 복잡하기 때문에 적절하지 않다고 한다.
제이쿼리가 단순히 옛날기술이라 나쁘다고만 생각했는데, 앞으론 이러한 이유로 쓰지 않는다고 당당하게 말할 수 있게 되었다 😎

리액트의 컨셉

정말 간단하다고 함(민태님피셜)

A와 B가 있는데 A는 약간 까탈스러운 편.
A를 다루다 보면 어느새 B도 까다로워짐.

이에 대한 해결책으로 중간에 쉬운 구조를 하나 만들어서 좁 더 복잡한 구조A와 연결을 해서 다루자는 것.
대표적인 예시가 바로 브라우저이다.


(민태님의 아이패드 그림을 소심하게 캡쳐 해보았다)

브라우저에 문자열을 보내기 위해 우리는 html태그를 쓴다.
원래 문자열은 구조가 없고 순수 데이터이기 때문에 다루기 어려운데,
다루기 쉬운 구조로 변환하고(DOM TREE) 우리는 그것을 내보낸다.

이러한 컨셉으로 만든 게 바로 리액트인 것이다!

자바스크립트에 DOM을 직접 연결하면 복잡하니까
자바스크립트 👉 VDOM 👉 DOM 형식으로 연결한 것..!

리액트 팀이 어느날 하늘에서 계시를 받고 유레카 외쳐서 새로운 것을 창조해낸게 아니라 결국은 브라우저 작동원리와 비슷하게 접근 한 것이다!

jsx의 탄생

자바스크립트 👉 VDOM 이렇게 넘어가는 과정이 쉽다고 했는데
babel이 컴파일하기 전의 모습대로 해야 한다면 아무도 개발하지 않을 것이다.
그래서 마크업 하듯이 편하게 jsx를 만들어 냈음
(처음엔 모양이 이상해서 욕먹었다고 한다 와..)

return문 안이 html이라고 생각하고 마크업을 해보면

function App() {
  return (
    //여기가 html이다 생각하고 마크업을 해보자
    <div>
      <h1>Hello?</h1>
      <ul>
        <li>React</li>
        <li>Redux</li>
        <li>TypeScript</li>
        <li>MobX</li>
      </ul>
    </div>
  );
}

이러한 모양새가 나오는데, 여기서 html태그 하나 하나가 컴포넌트라고 생각하면 된다.

짠~ 바벨이 컴파일 하기 전의 우측 코드를 보면 상당히 복잡해 보인다.
우리는 jsx가 있기 때문에 간편하게 좌측처럼 코드를 짤 수 있는 것이다.

컴포넌트화

ul, li 태그는 너무 일반화 되어 있고 의미가 없다. 말 그대로 목록을 출력하는 태그인 것이다.
그래서 컴포넌트화 해서 이름을 부여하면 의미가 생기게 되서 좋다.
마크업을 보고 어떤 데이터가 어디에 들어있는지 확인 하지 않아도 딱 컴포넌트만 봐도 읽기 쉬워지기 때문!

읽기 쉬워진다는 것은 곧 코드를 고치기 쉬워진다는 것 🙌

import React from 'react';
import ReactDOM from 'react-dom';

function StudyList() {
  return (
    <div>
      <h1>Hello?</h1>
      <ul>
        <li>React</li>
        <li>Redux</li>
        <li>TypeScript</li>
        <li>MobX</li>
      </ul>
    </div>
  );
}

function App() {
  return (
    <div>
      <h1>Hello</h1>
      <StudyList />
    </div>
  );
}

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

컴포넌트 이름을 StudyList라고 붙여주니 조금 더 와닿는다.

vdom의 작동원리

ui를 html로만 만들면 브라우저가 알아서 잘 그려준다.
그렇다면, 다루기 까다로운 문자열은 어떻게 할까?

  React.createElement('ul', {}, [React.createElement('li')]);

여기서 createElement는 버추얼 돔을 만드는 역할을 한다.
최종적으로 다 만들면 root컴포넌트에 appendChild 하는 식으로 가게 됨!
컴파일 되기 전의 우측 화면을 보자..

개발자는 생산자인 동시에 소비자기 때문에 예쁘고 편리한 것을 좋아한다.
매번 createElement를 쓰기 싫어서 vdom이 나오게 된 것이다!

const vdom = createElement('ul', {}, createElement('li', {}, 'React'));

🙋🏻‍♀️ 또 다시 돌아온 리빙포인트!!
babel에서 제일 상단에 /* @jsx 메소드명*/ 를 입력해주면 createElement가 설정한 메소드명으로 바뀐다.

👀 킹왕짱 중요한 포인트!! 👀
자바스크립트는 컴파일 타임이 없었다. 스크립트 언어인데 컴파일 타임이 새로이 생긴거고, 우리는 이를 인지해야 한다. 지금이 컴파일 타임인지 런타임인지 구분해야 한다.

다음에 시간 날 때 런타임, 컴파일 타임에 대해 정리해보고 싶다...!

매 세션이 그렇지만 늘 새로운 내용이다. 짜릿해
하지만, 이건 다 바벨 플로그인-리액트 플러그인 문서에 다 나와있는 내용이라고 하신다...
언제 한번 바벨 사이트도 파봐야겠다 💪

리액트 동작원리에 대해선 이 사람의 블로그도 정리가 잘 되어있다! 나중에 봐야지

컴포넌트를 대문자로 써야하는 이유

좌측과 우측을 비교해보자!
좌측은 우리가 jsx로 쓰는 형식이고, 우측은 컴파일 전의 모습이다
보면 같은 꺽쇠(</>) 안에 있어도 html태그와 컴포넌트의 컴파일 전 모습은 다르다.

<div> 같은 소문자들은 "div"라는 문자열로 인식이 되고,
<App> 같은 대문자, 즉 컴포넌트는 함수로 인식이 되고 대문자로 쓰여진다.
바로 이러한 이유 때문에 사용자가 리턴한 함수가 컴포넌트로 인식되는 것이다.

리액트 구현하기

function createElement(type, props = {}, ...children) {
  if(typeof type === 'function') {
    return type.apply(null, [props, ...children])
  }
  return { type, props, children };
}
  • createElement 함수에서 type이 함수라서 type()라고 쓸 수 있는데
  • 문제는 인자가 들어갈 게 많은데 어떤 형식으로 들어갈지 모르니 그렇게 쓰면 위험쓰
  • 그래서 type() 대신 apply 함수를 써줬음
  • 첫번째 인자는 컨텍스트가 없기 때문에 null을 넣어주었음
function renderElement(node) {
  const el = document.createElement(node.type);
  
  node.children.map(renderElement).forEach((element) => {
    el.appendChild(element);
  });
  // 노드에 칠드런이 있으면 맵을 돌려서 재귀호출!
  return el;
}

function render(vdom, container) {
  CredentialsContainer.appendChild(renderElement(vdom));
  // renderElement에서 리턴된 el가 여기에 붙는 것!
}
  • virtual dom은 재귀일 수밖에 없는 구조이다
  • root가 있고 자식이 있는데 이 자식이 언제 끝날지를 모른다
  • 그리고 부모와 자식이 똑같이 생겨서 전형적인 재귀호출 형태인 것!

늘상 있는 리액트의 문법대로만 코드를 작성하다가 이렇게 원리까지 파보게 되니 넘나 감회가 새롭다
근데 지금 내가 이걸 약간 이해하는 것도 리액트에 대한 선행학습이 이루어졌기 때문이라는 것을 잊지 말자!

리액트의 상태

  • 값 : 변하지 않음. 기본적으로 immutable
  • 상태 : 변할 수 있어야 함.

값의 상태를 바꾸기 위해 변수라는 걸 만들었고, 변수의 값을 바꿔준다
변수를 이용해 마치 값이 변하는 것 같은 착각에 빠뜨리는 것 그게 바로 상태!

클래스형과 함수형 컴포넌트의 차이

함수형 컴포넌트

함수형 App 컴포넌트는 함수이다.
함수의 스코프가 생기기 떄문에 App이 두번째 세번째 랜더 될때마다 상태가 바뀔수가 없다.
그래서 초기의 리액트에선 함수형 컴포넌트는 상태를 가질 수 없다고 했다.
(함수형은 호출되는 것 밖에 없어서 업데이트나 새로 만드는거나 똑같은 구조가 됨)

하지만 이제 훅스로 상태관리 할 수 있ablity!😎

클래스형 컴포넌트

그에 반해 class형 컴포넌트는 자연스럽게 자체 상태를 가질 수 있다.
클래스형 컴포넌트는 내부적으로 업데이트 되면필요한 부분만 호출할 수 있도록 라이프사이클 메소드가 존재한다.

  • 클래스형에서의 상태는 생성자 함수 안에 있어서 값을 변화
  • 라이프사이클 메소드들에 대한 설명은 생략! 공식문서에 잘 나와있으니깐!
  • setState함수를 안 쓰고 직접 상태값을 변화하면 리액트가 감지할 수 없어서 백날 해도 안 바뀌는 것임
    (프록시를 쓰면 직접 대입해도 값을 변화시킬 수 있음)

아래는 클래스형 컴포넌트가 어떻게 상태를 바꾸는지를 보여주는 pseudocode이다.

// 대충 이런식으로 상태변경이 이루어진다는 점

const hello = new Hello();
vdom = hello.render();
if (hello.hasOwnProperty('componentDidMount')) {
  hello.componentDidMount();
  
  // 이런 구조를 deligate라고 함.
  // 부모가 제어하는 구조! 
  // 적절한 타이밍에 적절한 메소드를 호출하게 됨. 그 적절한 타이밍을 라이프사이클이라고 함.
}

이러한 구조를 deligate라고 하는데, 부모가 제어하는 구조를 뜻한다.
위와 같은 로직으로 적절한 타이밍에 적절한 메소드를 호출하는데, 그 적절한 타이밍이 바로 라이프사이클인것!!

🙋🏻‍♀️ 오늘의 마지막 리빙포인트!!
vdom이 먼저 만들어지고 랜더되면 real dom이 생기는 순서로 앱이 그려지는데
리액트 팀에서 그들의 vdom은 다루기 쉬우니까 그 점을 이용해서
웹용은 리액트, 앱용은 네이티브로 정하게 된 것!
그래서 네이티브가 존재했구나 아하~ 👀

Hooks

함수형 컴포넌트에서 상태를 가질 수 있는 스펙을 Hooks라고 한다.
여러가지 Hook들이 있지만 그 중에서 가장 기본적인 useState를 알아보도록 하자

useState

const counter = result[0]
const setCounter = result[1];
  • 매번 이런식으로 작성 하면 번거로우니까 useState를 쓰는 것!
  • 기본적으로 배열을 리턴하는 구조니까 구조분해 할당(destructuring)을 이용한 것
  • 첫번째 요소론 값, 두번째 요소론 값을 고칠수있는 함수가 들어간다.

아래 이미지는 useState가 적용된 result를 콘솔에 찍어본 결과이다.

상태가 바뀌는 걸 어떻게 인지하지..?!

useState를 이용해서 상태를 바꾸는 건 알겠는데 그 원리를 한번 생각해보자.
어떻게 카운터 값이 증가하는 것일까?

👉정답은 타이밍!!

App 함수가 호출되고 특정 시점에 Hook 함수가 호출되면 이게 App에서 실행되었구나를 알 수 있게 된다.
그래서 초기값을 클로저로(사실 클로저는 아니고, 배열임) 묶고있는 값을 index를 알고 있는 함수를 생성해서 배열에 넣는것이 바로 Hooks의 작동 원리이다.
그래서 두번째 호출이면 초기값을 무시하고, 이전에 배열에 넣어둔 값을 호출하는것이다 와...

useState 뿐만 아니라 useEffect 등 다 이런 구조로 만들어져있다고 한다!

공식문서에 나온 Hooks 규칙 중 하나

  • 리액트 함수 내부에서만 Hook을 호출해야 한다.

리액트 컴포넌트를 인덱스로 잡기 때문에 꼭 내부에서 호출해야 하는 것!
(멋모르고 훅을 시도해봤을때, 컴포넌트 밖에서 호출한 경험이 있.... 🤣)

이거 말고도 다양한 규칙들이 있는데 괜히 있는 규칙이 아니다. 다시 공식문서를 읽어봐야 헦사.
컴포넌트의 호출 순서에 따라 훅이 발동되기 때문에 규칙에 맞게 쓰지 않으면 엉뚱한 훅에 레퍼런스가 걸릴 위험이 있기도 하다.

급 마무리 하자면.. Hooks 덕에 조금 더 선언적인 프로그래밍이 가능하게 되었다는 훈훈한 결말!

profile
Dooreplay! 안 되면 될 때까지,

2개의 댓글

comment-user-thumbnail
2020년 9월 9일

두리형 잘 보구 가요^^

답글 달기
comment-user-thumbnail
2020년 9월 9일

vdom에 대해 조금 더 잘 알게 되엇읍니다 ^^

답글 달기