-
✔ 우아한 테크러닝 3기: React & TypeScript 4회차우아한 테크러닝 3기 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> ); } }
- 여기서
super
는extends
뒤에 있는 것이고 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());
콘솔에 찍힌 값을 확인해보면
value
는yield
로 반환된 값이며done
은true
일 경우 제너레이터가 종료된다.
즉,yield
나return
이 나올 때 까지 실행하는 것이다.
제너레이터는
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
에 매개변수로 넘겨준 문자열a
가yield
의 반환값으로 담겨서 사용할 수도 있는 것을 확인할 수 있다.
제너레이터를 활용해서 비동기함수를 동기적으로 나타낼 수 있다.
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/await
는Promise
에 최적화 되어있다.그렇지만 제너레이터를 사용할 경우
Promise
형태의 값이 반환되지 않아도 사용이 가능하기 때문에 더 다양한 상황에서 응용이 가능하다.전체 소스는 GitHub를 참고 🙏
🚀 4회차 후기
오늘도 역시나 역시 배운게 많았다. 생소한 용어도 많이 있었다. 찾아서 공부를 더 해봐야겠다.😤
다음 주부터는 typescript에 대해서 배운다는데 집중해서 들어야겠다..📖
벌써 반이나 와버렸다.. 시간이 너무 빠르다. 블로깅하는 것이 쉽지않은데 끝까지 아자아자! 해야겠다!!!✨'우아한 테크러닝 3기' 카테고리의 다른 글
✔ 우아한 테크러닝 3기: React & TypeScript 6회차 (0) 2020.09.20 ✔ 우아한 테크러닝 3기: React & TypeScript 5회차 (4) 2020.09.19 ✔ 우아한 테크러닝 3기: React & TypeScript 3회차 (0) 2020.09.09 ✔ 우아한 테크러닝 3기: React & TypeScript 2회차 (0) 2020.09.07 ✔ 우아한 테크러닝 3기: React & TypeScript 1회차 (0) 2020.09.02