✔ 우아한 테크러닝 3기: React & TypeScript 3회차
✌ 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
이 리액트 테그를 변환해주는 것이다. 그렇기 때문에 실제로는 결국 그냥 함수 호출에 불과하다.ReactDOM
은render
정적 메소드를 갖는데 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 }; }
위에서 만들어본
vdom
을createElement
를 이용하여 만들 수 있다.// 예시 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 시간..)
내일이면 반을 했는데 끝나면 아쉬울거 같다. 열심히 듣자.😤