개발 공부/React

가장 쉬운 리액트 1~4강

5묘 2022. 12. 13. 16:34

강의를 들은 이유: React 문법을 잘 알고있단 확신이 없어서😅
그동안 React를 쓸 때 내가 정말 React 문법을 깊이 있게 이해하고 있다는 확신이 들지 않아 공식 문서를 쉽게 한국어로 풀어서 강의해주시는 데이비드님의 '가장 쉬운 React(https://youtu.be/MTzS8XnD6-Q)'강의를 보게 됨.

[가장쉬운 리액트] Elements - 가장 쉬운 웹개발 with Boaz

공식문서는 여기.
https://reactjs.org/docs/rendering-elements.html

 

Rendering Elements – React

A JavaScript library for building user interfaces

reactjs.org

 


1강: JSX

 

1.JSX

(React 공식문서의 main concepts 번역 글입니다.) 함께보면 이해가 쏙쏙https://youtu.be/-LtBRRnHvJ0 JSX는 React element를 나타내는 javascript의 확장된 문법이다.React element란? DOM에 Render 될 객체이다.왜 JSX를 사

davidhwang.netlify.app

더보기

React는 렌더링 로직이 UI 로직과 본질적으로 결합되어 있다는 사실을 고려하여 만들어진 라이브러리다.
(이벤트가 다뤄지는 방법, state가 바뀌거나 data가 display되는 과정을 통해 렌더링 로직과 UI 로직이 결합되어 있음을 알 수 있다.)

즉, React는 렌더링 로직 + UI 로직을 통합 수행하는 Component를 만든다.
(Component는 React element을 return하는 function. React element는 Dom에 render 객체(object))

JSX는 이 React elemet를 표현하는 Markup Language의 일종

JSX를 사용하면 UI 로직을 시각적으로 이해하기 쉽게 표현할 수 있다.
-> HTML tag와 모양 비슷

React로 프로그래밍하기 위해 반드시 JSX를 사용해야 하는 것은 아니다.
-> React.createElement()를 통해 객체(element) 생성 가능

*** JSX는 object(React element)를 나타낸다.

const element = (
  <h1 className="greeting">
    Hello, world!
  </h1>
);

위의 jsx 코드를 Babel이 컴파일하면, 아래 코드가 실행된다.

const element = React.createElement(
  'h1',
  {className: 'greeting'},
  'Hello, world!'
);

Babel은 JSX를 컴파일하기 위해 위 코드처럼 React.createElement()를 실행하고 이때 arguments로는

  • HTML tag의 이름
  • classname등이 포함된 object
  • children

이 들어간다.

const element = {
  type: 'h1',
  props: {
    className: 'greeting',
    children: 'Hello, React!'
  }
}

Babel이 컴파일한 결과 위 object가 생성된다. 즉, React.createElement()가 실행된 결과로 element라는 변수에 위 object가 assign된다. 이 object가 바로 React element이다.

React element = JSX가 나타내는 것 = React.createElement()의 return값

React element의 구성은 아래와 같다.

  • type의 value값인 HTML tag 이름
  • props의 value값인 className children(또한 추가적인 data)

JSX에 javascript 구문 삽입하기

javascript의 모든 구문은 {}(curly bracket)을 사용해서 JSX에 삽입할 수 있다.

const name = 'Josh Perez';
const element = <h1>Hello, {name}</h1>;
ReactDOM.render(
  element,
  document.getElementById('root')
);

element라는 변수에 할당한 JSX안에서 name변수를 삽입했다.
변수 외에도 함수, 객체 속성등 모든 javascript표현이 삽입 가능하다.

function formatName(user) {
  return user.firstName + ' ' + user.lastName;
}

const user = {
  firstName: 'Harper',
  lastName: 'Perez'
};

const element = (
  <h1>
    Hello, {formatName(user)}!  </h1>
);

ReactDOM.render(
  element,
  document.getElementById('root')
);

JSX도 javascript 표현식이다

JSX가 Babel에 의해 compile 되면 react element(객체)를 반환하는 함수이다.
표현식은 단일, 특정한 결과값으로 계산되는 것을 말한다. 따라서 JSX는 함수, 즉 javascript 표현식이므로 반복문, 조건문 등에서 활용 가능하다(변수에 할당도 가능)

function getGreeting(user) {
  if (user) {
    return <h1>Hello, {formatName(user)}!</h1>;  }
  return <h1>Hello, Stranger.</h1>;
}

JSX의 attribute 값 정하기

아래와 같이 tabIndex라는 attribute의 값을 string 0으로 줄 수 있다.

const element = <div tabIndex="0"></div>;

{}를 사용하여 attribute에 javascript expression을 넣을 수도 있다.

const element = <img src={user.profileImageUrl} />;

JSX는 HTML보다 javascript에 가깝기 때문에 attribute의 변수명으로 camel case를 사용한다.

JSX의 children 값 정하기

만약 tag의 children이 없다면 />를 활용해서 바로 닫을 수 있다.

const element = <img src={user.avatarUrl} />;

또한 JSX는 아래처럼 children('Hello!', 'Good to see you here.')을 가질 수도 있다.(<h1>, <h2>)

const element = (
  <div>
    <h1>Hello!</h1>
    <h2>Good to see you here.</h2>
  </div>
);

JSX는 Injection Attacks로부터 안전하다

아래와 같이 사용자 input을 JSX안에 넣는 것도 안전하다.

const title = response.potentiallyMaliciousInput;
//This is safe
const element = <h1>{title}</h1>;

React DOM은 렌더링하기 전에 JSX안에 넣은 모든 값을 escape 한다. escape 한다는 것은 '모든 것은 렌더링되기 전에 string으로 변한다. 가령 <는 '&lt'로 변하는 식이다.'는 의미이다. 그 결과, XSS 공격을 방지 할 수 있다.


xss 공격이란? :  악성 사용자가 javascript의 구문을 입력하여 실행되게 만드는 공격으로 자세한 내용은 아래 링크를 참조한다. https://brownbears.tistory.com/250

2강: Elements

 

2.Element

(React 공식문서의 main concepts 번역 글입니다.) 함께보면 이해가 쏙쏙https://youtu.be/MTzS8XnD6-QReact element는 UI를 표현하는 결과물이다.React element는 3가지 특징을 가진다. React앱의 가장 작은 구성 요소Pla

davidhwang.netlify.app

더보기
  • element는 React 앱의 가장 작은 구성 요소
  • Plain object (브라우저 DOM element과 다르게)
  • => 그래서 브라우저 DOM element와 비교해 생성하는데 비용이 적게 든다

React DOM은 React element와 DOM element가 일치 하도록 DOM을 업데이트하는데 이 업데이트 과정에서
React element는 다시 렌더링된다.

React element가 DOM에 렌더링되는 과정

React element를 DOM에 처음 렌더링하기 위해서 아래 과정을 거친다.

  1. React element들이 DOM element에 소속된다.
  2. 브라우저가 소속된 React element들을 DOM element로 인식한다.
  3. DOM element로 인식된 React element들을 렌더링한다.

그럼 React element들은 어떻게 DOM element에 소속될까?

<div id='root'></div>

React element들은 이 div(DOM element)에 소속된다.
 div의 이름은 ‘root’ 즉, 모든 React element의 뿌리이다. (이 div를 root element, 혹은 root DOM element라 부른다)
div에 React element들이 소속되는 과정을
아래 코드를 통하여 자세하게 알아보자.

const element = <h1>Hello, React</h1>;
ReactDOM.render(element, document.getElementById('root'));
  1. ReactDOM은 render라는 method를 실행시킨다.
  2. 이때 arguments는 두가지
    • React element
    • 브라우저 DOM element
  3. render method 실행 결과 React element가 DOM element에 소속된다.
    • React element가 html DOM element로 인식됨
  4. 이제 DOM element로 인식된 React element가 렌더링된다.

렌더링 된 React element 업데이트하기

React element는 immutable 하다. 그러므로 아래 두가지 특징을 가진다.

  • element가 생성된 후에는 children과 props를 변경할 수 없다
  • 특정 시점의 UI를 나타낸다

그러므로 React element를 업데이트하는 방법은 1가지 뿐이다.

새로운 React element를 만들고, ReactDOM.render()에 넣어 실행시킨다.

아래 예시를 보자

function tick() {
  const element = (
    <div>
      <h1>Hello, world!</h1>
      <h2>It is {new Date().toLocaleTimeString()}.</h2>
    </div>
  );
  ReactDOM.render(element, document.getElementById('root'));
}

setInterval(tick, 1000);

위 코드를 실행시키면 setInterval에 의해 매초마다 ReactDOM.render()가 실행되는 것을 확인할 수 있는데 이것은 일반적인 방법은 아니다.

중요 : 대부분 실제 React app은 ReactDOM.render()를 한번만 실행한다.

React element의 달라진 부분만 업데이트

만약 ReactDOM.render()이 실행될 때마다 root DOM node에 소속된 모든 React element들이 다시 렌더링 된다면 너무 비효율적이다. ReactDOM은 React element의 달라진 부분만 업데이트한다.

function tick() {
  const element = (
    <div>
      <h1>Hello, world!</h1>
      <h2>It is {new Date().toLocaleTimeString()}.</h2>    
    </div>
  );
  ReactDOM.render(element, document.getElementById('root'));
}

setInterval(tick, 1000);

위 예제 코드를 실행시키면 '<h2>It is {new Date().toLocaleTimeString()}.</h2>' text node(달라진 부분) 만 업데이트 된다. 

UI(React element)가 매 순간 어떻게 보여야할지를 고민하자.
앞으로 어떻게 변화할지를 미리 고민하지 말고!

위에서 ReactDOM.render()가 대부분 한번만 실행된다고 했던 것 기억나는가?  3.component, 4.state에 대해 공부하고나면
어떻게 한번만 실행될 수 있는지 그 원리를 알 수 있게될 것이다.


더보기


++ 내가 듣고 있는 강의가 2019년(3년 전) 버전이라, ReactDOM.render(react element, 브라우저 DOM 엘리먼트) 은 레거시 코드가 됐고 아래와 같이 createRoot()를 쓰게 됐다고 한다.(v.18부터 사용 가능)

둘의 차이를 정리해 둔 블로그(https://avocado12-note.tistory.com/41)
(https://github.com/reactwg/react-18/discussions/5)

기존의 reactDOM.render에 react Element와 root DOM Element(container)를 넣었을 때는 container를 렌더링 할때마다 전달해야 했다.(DRY 원칙에 어긋난다)

import * as ReactDOM from 'react-dom';
import App from 'App';

const container = document.getElementById('app');

// Initial render.
ReactDOM.render(<App tab="home" />, container);

// During an update, React would access
// the root of the DOM element.
ReactDOM.render(<App tab="profile" />, container);

새로운 코드를 쓰면 container를 렌더링 할 때마다 전달하지 않아도 된다.

import * as ReactDOM from 'react-dom';
import App from 'App';

const container = document.getElementById('app');

// Create a root.
const root = ReactDOM.createRoot(container);

// Initial render: Render an element to the root.
root.render(<App tab="home" />);

// During an update, there's no need to pass the container again.
root.render(<App tab="profile" />);

 createRoot()를 쓰는 방식이 hydrate() 를 사용하지 않고, hydrateRoot()로 create와 render을 한번에 할 수 있어 더 효율적이라는데, 이에 대해 조금 더 조사해봐야 될 것 같다.🤔

내가 했던 작업을 살펴보니 SSAFY 때 만든 프로젝트(react v.17)는 레거시 코드로 작성해서 인자를 두개 넣어주었다. 하지만 최근 만든 해커톤 프로젝트(react v.18)에서는 createRoot()을 써서 index.js가 만들어져있다.

legacy code로 작성한 index.js (치츄 프로젝트)
new code로 작성한 index.js (컬리 해커톤 프로젝트)

리액트 18에서 변경된 더 많은 부분들은, 우선 17이하 버전의 기본문법 강의를 먼저 다 듣고 나중에 살펴볼 예정.
https://velog.io/@woodong/React-18-%EC%A3%BC%EC%9A%94-%EB%B3%80%EA%B2%BD%EC%A0%90

노마드 코더 - React 18 변경점

3강: Components & Props

 

3.Components and Props

(React 공식문서의 main concepts 번역 글입니다.) 함께보면 이해가 쏙쏙https://youtu.be/MhjaBokqfVwComponents는 props를 argument로 받아element를 리턴하는 function이다.(여기서 props는 element가 보여줄 data이다) compoen

davidhwang.netlify.app

더보기

Components는 props를 argument로 받아 react element를 리턴하는 function이다.
(여기서 props는 element가 보여줄 data이다)
compoents를 활용하여 element를 독립적으로 만들 수 있다. 이는 element의 재사용성을 높여준다.
(마치 super class 처럼)

Component 선언 방법 2가지

일반적으로 component를 선언할 때 이름은 대문자로 시작한다.
선언 방법은 functional component class component 2가지가 있다.

Functional component

component는 아래처럼 function으로 만들 수 있다.

function Welcome(props) {
  return <h1>Hello, David</h1>;
}

react component가 되려면 아래 2가지 기준을 만족해야 한다.

  • 하나의 props(property의 약자로 data object)를 argument로 받는다.
  • react elementreturn 한다.

위에 function Welcome은 2가지 조건을 만족하기 때문에 component이다.

Class component

또한 ES6부터 추가된 class keyword를 사용해서 component를 만들 수도 있다.

class Welcome extends React.Component {
  render() { //클래스 메서드
    return <h1>Hello, {this.props.name}</h1>;
  }
}

function과 class 두가지 방법은 동일하게 react component를 만들 수 있다.

두가지 방법 사이 유일한 차이점은 state 관리 기능이었다. 
(과거에는 class만 state 관리 가능)
하지만 react v.16부터 react hooks가 등장하면서
functional component에서도 state 관리가 가능 해졌다.

Rendering a component

element 역할의 변화

이전에 react element는 단순히 HTML tag로 만들어졌다.

const element = <div />;

그러나 component를 활용해서 아래처럼 element를 만들게 되었다.

const element = <Welcome name="david" />;

component rendering 과정

react가 이러한 component를 실행할 때,
JSX attribute(예를 들어, 위 코드에서 name)을
single object ({name: "david"})의 형태로
component에게 전달한다.
이 object가 바로 props이다.

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

const element = <Welcome name="Sara" />;
ReactDOM.render(
  element,
  document.getElementById('root')
);

위 코드를 실행하면 아래의 과정을 거친다.

  1. ReactDOM.render( ~~~ )를 실행한다.
  2. render()안에 element인자인 Welcome comoponent를 실행한다. (이때 props인자는 {name: “david”} object)
  3. Welcome component는 <h1>Hello, david</h1>라는 element를 return한다.
  4. ReactDOM은 element의 달라진 부분(name의 value)이 일치하도록 DOM을 업데이트한다.

component 구성하기

component는 다른 component를 조합하여 구성할 수 있다. 즉, component는 추상화(abstraction) 할 수 있다.
그 결과 component는 거꾸로 추출할 수도 있다.
(부모 component에게 소속된 자식 component를 만들 수 있으므로)

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

function App() {
  return (
    <div>
      <Welcome name="David" />
      <Welcome name="Paul" />
    </div>
  );
}

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

위 코드에서 처럼 App component를 구성할 때
그 안에서 많은 Welcome component들을 조합하여 구성할 수 있다.

처음부터 react로 app을 만드는 경우
최상위 component인 App component부터 가장 먼저 만들게 된다.
하지만 기존 앱을 react로 바꿀때는 더 작은 component부터 만들기도 한다.

component 추출하기

component는 핵심 component를 추출할수록 간단해진다.
(이때 추출한 component가 리턴한 object는
OOP에서 super class와 비슷하다.)

function Comment(props) {
  return (
    <div className="Comment">
      <div className="UserInfo">
        <img className="Avatar"          
          src={props.author.avatarUrl}
          alt={props.author.name}
        />
        <div className="UserInfo-name">
          {props.author.name}
        </div>
      </div>
      <div className="Comment-text">
        {props.text}
      </div>
      <div className="Comment-date">
        {formatDate(props.date)}
      </div>
    </div>
  );
}

 Comment component는 재사용하기 어렵다.
너무 많은 component들을 special하게 갖고 있기 때문이다.
component를 추출할 때 그 기준은 재사용성이 높은 코드인가? 이다. (추출한 component는 재사용성이 높아서 코드의 중복을 제거함)

가장 먼저, 하이라이팅된 Avatar component를 추출하자.

function Avatar(props) {
  return (
    <img className="Avatar"
      src={props.user.avatarUrl}
      alt={props.user.name}
    />

  );
}

원래 Avatar component의 props 이름은 author였다.
하지만 이름을 user로 바꾸었다.
그 결과, author 뿐만 아니라 다른 직업도 Avatar component를 활용할 수 있게 되었다.

component의 props 이름은 해당 재사용성을 높이는 방향으로 짓자.
(역할을 구체적이고 명확하게 고려해서 짓지 말자 ❌)

function Comment(props) {
  return (
    <div className="Comment">
      <div className="UserInfo">
        <Avatar user={props.author} />        
        <div className="UserInfo-name">
          {props.author.name}
        </div>
      </div>
      <div className="Comment-text">
        {props.text}
      </div>
      <div className="Comment-date">
        {formatDate(props.date)}
      </div>
    </div>
  );
}

위와 같이 Avatar component의 props이름이 user가 되었고,
comment component는 조금 간단해졌다.

다음은 UserInfo component를 추출해보자.

function UserInfo(props) {
  return (
    <div className="UserInfo">
      <Avatar user={props.user} />
      <div className="UserInfo-name">
        {props.user.name}
      </div>
    </div>
  );
}

UserInfo 또한 comment component 외에 다른 component에서 재사용 가능하므로
추출해서 중복된 코드를 제거한다.

function Comment(props) {
  return (
    <div className="Comment">
      <UserInfo user={props.author} />
      <div className="Comment-text">
        {props.text}
      </div>
      <div className="Comment-date">
        {formatDate(props.date)}
      </div>
    </div>
  );
}

최종적으로 comment component는 위와 같이 간단해졌다.
재사용할 수 있는 component들을 추출했기 때문이다.

component 추출은 마치 generalization과 비슷하다. 공유되는 특성을 추출하는 것과 비슷하게 중복되는 로직을 추출하기 때문이다.

Props 는 읽기만 가능!

function 또는 class로 component를 만든 경우, 절대 주어진 props를 수정할 수 없다.
아래 function을 보자.

function sum(a, b) {
  return a + b;
}

sum function은 pure하다.
왜냐하면 argument로 주어진 a,b값을 수정하지 않기 때문이다.
그 결과, 언제나 동일한 값을 return한다.

반대로 아래 function은 pure하지 않다.

function withdraw(account, amount) {
  account.total -= amount;
}

왜냐하면 위 function은 argument로 주어진 account의 값을 바꾸기 때문이다.
React에서는 절대적인 규칙이 한가지 있다.

** 모든 component는 props 값을 수정하지 않는 pure function이다.

물론 UI는 동적이어서 나타내는 값이 변한다.
이와 관련된 state라는 개념을 다음 블로깅에서 만나보자. state는 component가 React의 절대적인 규칙을 지키면서
동시에 event, server와의 통신 등 상호작용을 통해 UI가 나타내는 값이 변할 수 있도록 해준다.


4강: States and lifecycle

 

4.States and Lifecycle

(React 공식문서의 main concepts 번역 글입니다.) 함께보면 이해가 쏙쏙https://youtu.be/xJSNDGEvMwEhttps://youtu.be/jAVTHS3YGNIhttps://youtu.be/isCp8-dP050state는 component의 상태이다.props가 component에게 주어지는 data라면s

davidhwang.netlify.app

더보기

state는 component의 상태이다.
props가 component에게 주어지는 data라면
state는 이 props의 값이 변할 때
변하는 props의 data를 component가 스스로 업데이트하여
UI로 표시할 수 있게 한다.

lifecycle은 component가
만들어지고 렌더되고 사라지는 일련의 과정이다.

이 lifecycle과 state는 어떠한 연관이 있을까?
아래 계속 변하는 props를 가진 tick component 예제를 통해
lifecycle과 state의 관계 
component가 스스로 업데이트하는 과정에 대해 알아보자

변하는 props값을 가진 tick component

우리는 props로 주어지는 data 값의 변화를
UI에 업데이트하는 방법을 1가지 배웠다.

// React v.18
const root = ReactDOM.createRoot(document.getElementById('root'));
  
function tick() {
  const element = (
    <div>
      <h1>Hello, world!</h1>
   ** <h2>It is {new Date().toLocaleTimeString()}.</h2>
    </div>
  );
  root.render(element);
}

setInterval(tick, 1000);

그 방법은 아래의 과정을 거친다.

  1. 바뀐 props를 argument로 주어 component를 실행한다.
  2. component가 return한 새로운 element를 ReactDOM.render()에게 argument로 전달한다.
  3. ReactDOM.render()를 다시 실행한다.(하이라이팅된 부분)

값이 변할때마다 위 과정을 모두 거치는 것은 비효율적이다.
우리는 component가 변하는 props값을 스스로 업데이트하길 원한다.
어떻게 가능할까?
props 값이 변하는 것을 UI에 업데이트하는 방법은 1가지가  있다. 아래 tick component를 통해 그 방법을 알아보자.

encapsulating tick component

tick component는 Clock component를 extract하여
렌더링 함수와 react element을 분리할 수 있다.

function Clock(props) {
  return (
    <div>
      <h1>Hello, world!</h1>
      <h2>It is {props.date.toLocaleTimeString()}.</h2>
    </div>
  );
}

function tick() {
  ReactDOM.render(
    <Clock date={new Date()} />,
    document.getElementById('root')
  );
}

setInterval(tick, 1000);

Clock component 즉, tag가 포함된 react element를 extract했다. 그 결과, UI요소인 tag들이 encapsulated 되어
어떤 tag를 포함하고 있는지 보이지 않게 되었다.
기존에 tick component에는 렌더링 함수만 남아있게 되었다.

하지만 아직 해결하지 못한 문제가 있다.
Clock component에게 props로 date 값이
고정적으로 주어지고 있으므로(하이라이팅된 부분)
매초마다 바뀌는 date 값을 표시하기 위해 setInterval로 tick을 계속 다시 실행해야(즉 통째로 렌더링을 매번 다시 해줘야)하는 문제가 남아있다.

Clock component가 스스로 업데이트하길 원해

우리는 Clock component에게 date값을 props로 주지 않아도
Clock component가 스스로 업데이트 하길 원한다. 마치 아래 코드에서 '<Clock />' 부분처럼!

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

위 코드처럼 변하는 props를 반복해서 주지 않아도
Clock component가 스스로 업데이트 하기 위해서는 Clock component에게 state를 주어야한다.

state는 props와 비슷하지만 private하고 component에 의해 완전하게 controll 된다.

state를 주기 위해서는 Clock component가 function에서 class로 바뀌어야 한다.=> React v.16부터는 hooks를 사용해 state 관리 가능하다. 그러나 이번엔 class를 이용해 state를 바꾸는 것만이 가능하다는 전제를 깔고 배워보자.(그럼 hook이 얼마나 간편한지 깨닫게 될테니까!)

function component를 class component로 바꾸기

아래와 같은 과정을 통해 function에서 class로 바꿀 수 있다.

  1. class keyword를 사용해서 같은 이름의 component를 선언한다 (이때 해당 component는 React.component에게 inheritance 받는다)
  2. render() method를 class 안에서 선언한다 (render() 메소드는 state가 변경될 때마다 재실행되는 메서드이다!)
  3. render할 element를 return해주는 부분을 render()안으로 옮긴다
  4. render()안에서 props를 this.props로 바꾼다
  5. 기존의 function component를 지운다
class Clock extends React.Component {
  render() { // render() 메소드는 state가 변경될 때마다 재실행되는 메서드이다!
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.props.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

위에 render() method는 업데이트 될때마다 실행된다.
하지만 같은 DOM node에서 Clock을 render한다면
Clock class는 한개의 instance를 만들고
그 instance가 계속 사용되어 진다.

그 결과 state, lifecycle method 등의
추가기능을 사용할 수 있다.

class component에 state 추가하기

아래 과정을 통해 date라는 props를 state로 바꿀 수 있다.

  1. this.props.date를 this.state.date로 바꾼다 
class Clock extends React.Component {
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>      </div>
    );
  }
}

2. class constructor를 선언하고 state에 초기값을 준다.

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};  
    }

  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

constructor에 props를 상속한다.
(class component는 항상 super(props)로 부모 class로부터 props를 상속 받아서 초기화해 시작한다)

constructor(props) {
  super(props);  // 부모 component로부터 전달받은 props를 초기화함.
  this.state = {date: new Date()};
}

3. 기존 Clock component에 props로 주었던 date를 지운다.

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

나중에 바뀌는 시간을 표시하기 위한 코드를 Clock component에 직접 추가한다.

최종적인 결과는 아래와 같다.

class Clock extends React.Component {
  constructor(props) {    
      super(props);    
      this.state = {date: new Date()};  
  }
  
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>      
       </div>
    );
  }
}

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

이제 바뀌는 시간을 표시하기 위한 코드를 추가하자 그 결과, Clock component는 스스로 업데이트 할 수 있게 될 것이다.

class component에 lifecycle method 추가하기

Clock component가 DOM에 반복해서 render되는 순간을 캐치하기위해 타이머를 설정해야 한다.
component가 DOM에 렌더되는 순간을 mounting 라고 한다. => 컴포넌트가 DOM에 소속되며 브라우저에 DOM 요소로 인식되는 순간!

또한, 이 Clock component가 만든 DOM이 삭제될 때
 타이머도 같이 삭제되어야 한다.(메모리 관리를 위해)
component가 DOM에서 삭제되는 순간을 unmounting 라고 한다.

lifecycle method(해당 라이프사이클 시점에 실행됨)를 활용하면
이 타이머가 울릴 때, 실행되어야 할 코드를 추가할 수 있다.

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  componentDidMount() {
  	// Mount 직후에 실행
  }

  componentWillUnmount() {

  }

  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

componentDidMount와 componentWillUnmount라는 lifecycle method를 위와 같이 추가했다.

componentDidMount

component가 return한 element가 DOM에 render된 후 실행되는 method이다.

아래 코드를 추가해주면
Clock component가 DOM에 render된 후 tick을 반복 실행하는 타이머가 설정된다.

componentDidMount() {
  this.timerID = setInterval(
    () => this.tick(),
    1000
  );
}

이때, timerID는 바로 this.timerID로 저장할 수 있다.

data flow와 연관 있는 것들(props, state)이 아닌 다른 것들(timerID)은 얼마든지 this에 바로 저장할 수 있다.

componentWillUnmount

component가 return한 element가 DOM에서 제거된 후 실행되는 method이다.

componentWillUnmount를 활용하면 설정했던 타이머를 제거할 수 있다.
(component가 제거되면 관련된 resource(timer 등)를 함께 제거해야 메모리 낭비가 없다.)

componentWillUnmount() {
  clearInterval(this.timerID);
}

tick component를 Clock component안에 넣기

tick은 기존에 렌더링 함수를 분리한 것이었다.
하지만 state 값, 즉 바뀌는 시간을 수정하는 method가 되어야 한다.

tick() {
  this.setState({
    date: new Date()
  });
}

setState라는 function을 통해 state값을 수정할 수 있다.
(setState에 대한 자세한 설명은 아래에서 추가로 하겠다)

완성된 Clock component(state & lifecycle method 추가)

우선 완성된 clock component의 코드를 보자

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  componentDidMount() {
    this.timerID = setInterval(
      () => this.tick(),
      1000
    );
  }

  componentWillUnmount() {
    clearInterval(this.timerID);
  }

  tick() {
    this.setState({
      date: new Date()
    });
  }

  render() { // state 값 달라지면 render() 실행
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

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

ReactDOM.render()가 아예 밖으로 빠졌다.
Clock component는 class로 선언되어
state와 lifecycle method가 추가될 수 있었다.

위 코드가 실행되는 과정은 아래와 같다.

  1. ReactDOM.render()가 실행되면 React는 <Clock />를 call한다.
  2. Clock component의 constructor가 실행된다.
  3. Clock이 return하는 element, 즉 Clock class의 instance는
    super(props)를 통해 Clock의 props를 상속받는다.
  4. this.state에게 현재 시각(new Date()의 return 값)이 할당된다.
    (나중에 이 state 값은 업데이트 된다)
  5. React가 Clock component의 render()를 call한다.
    (이 과정을 통해 React는 무엇을 화면에 표시해야하는지 알아차린다)
  6. React는 Clock component의 render()의 return값과
    일치하도록 DOM을 업데이트 한다.
  7. Clock component의 render()의 return값이 DOM에 소속되면
    즉, DOM이 업데이트되면 React는 componentDidMount()를 실행한다.
  8. componentDidMount() 안에서 Clock component는 브라우저에게
    tick function을 1초에 1번 call하도록 타이머 설정을 요청한다
  9. 1초에 1번 브라우저는 tick function을 call한다.
    tick function 안에서 Clock component는
    새로운 현재 시간 obejct를 argument로 받아 setState를 call한다.
  10. setState를 call했기 때문에 React는 state 값이 변했음을 안다.
    state가 변하면 React는 Clock component의 render()를 다시 call한다.
    (무엇을 화면에 표시해야하는지 그 차이를 React가 알아차리기 위해)
  11. 이번에는 render() 안에 this.state.date 값이 달라질 것이다.
    따라서 render()의 return값이 state의 변화를 반영하여 업데이트 되고
    그 결과, React도 render()의 return값의 변화를 반영하여 DOM을 업데이트한다.
  12. 만약 Clock component가 DOM에서 제거된다면
    React는 componentWillUnmount를 실행하고 타이머 역시 제거된다.

state에 대해 주의해야 할 부분들

state를 수정하는 유일한 방법

반드시 setState()를 사용하자.

절대 state에 수정된 값을 직접 할당하지 마라

state에 값을 직접 할당할 수 있는 곳은
constructor안에서 초기값을 할당할 때 뿐이다.

setState는 비동기이다

리액트는 같은 object를 argument로 받은 setState를
여러번 call하면 한번만 실행한다.(동일한 작업 여러번 시키는 걸로 인식하므로 performance를 위해서 1번만 실행시킨다.)

this.props와 this.state는 비동기적으로 업데이트 되기 때문에
다음 state값을 계산할 때 현재 props와 state값을 사용하면 안된다.

아래 코드는 state를 완벽하게 업데이트할 수 없다. setState를 여러 번 실행하더라도 같은 object를 가리키기 때문에 반복적으로 작업하는 걸로 인식해 1번만 실행되기 때문이다.

// Wrong
this.setState({
  counter: this.state.counter + this.props.increment,
});

이 문제를 해결하기 위해서는 setState의 argument로 object 대신 function을 주어야 한다.

// Correct
// 같은 callback을 여러번 넘기더라도 비동기로 백그라운드로 가서 쌓였다가
// task queue로 옮겨져 call stack이 비면 순차적으로 처리되기 때문이다. 
this.setState((state, props) => ({
  counter: state.counter + props.increment
}));

이 function은 바뀌기전 state값 업데이트 될 때 props값을 argument로 받는다.

state의 업데이트는 merge된다

setState()를 call하면 React는 setState()의 argument로 받은 object를 현재 state 값에 merge한다.

아래처럼, state값이 몇개의 key-value 페어로 이루어져 있다면

constructor(props) {
  super(props);
  this.state = {
    posts: [],
    comments: []
  };
}

setState()를 여러번 call해서 state안에 각 key-value 페어를 수정할 수 있다.

componentDidMount() {
  fetchPosts().then(response => {
    this.setState({
      posts: response.posts
    });
  });

  fetchComments().then(response => {
    this.setState({
      comments: response.comments
    });
  });
}

이때 merge는 shallow copy이다.
따라서 this.setState({comment}) 
오직 comments만 수정한다.(posts는 수정하지 않는다.)

=> 이게 뭔 말이냐면 setState() 함수에서 얕은 병합과 깊은 병합이 발생하는데, 얕은 병합은 변경 사항을 기존 state에 덮어씌우는 방식으로 병합을 하고, 깊은 병합은 state의 모든 필드(전체)를 변경하지 않고, 변경이 일어난 필드에 대해서만 병합을 한다.

두가지 병합의 예시는 아래와 같다.(출처:https://www.nextree.io/003-react-state/)

   ...
   constructor(props) {
        super(props);
        this.state = {
            name: {
              product: 'Coke',
              manufacturer: 'Coca_cola'
            },
            price: 1000,
            quantity: 10
        }
    }
  	//얕은 병합
    modifyProduct = () => {
        this.setState({
            name: {product:'Fanta'}
        });
    }
    /*  state는 아래와 같이 병합됩니다.        
    		name: {
              product: 'Fanta',
            },
            price: 1000,
            quantity: 10
        }
    */
    
...
    
    //깊은 병합
    modifyProduct = () => {
        this.setState({
            name: {
              product:'Fanta',
          	  manufacturer: 'Coca_cola'
            }
        });
    }
    /*  state는 아래와 같이 병합됩니다.        
    		name: {
              product: 'Fanta',
              manufacturer: 'Coca_cola'
            },
            price: 1000,
            quantity: 10
        }
    */
    ...

setStste() 함수는 기본적으로 얕은 병합을 한다. 얕은 병합은 state 내부 객체를 변경하면 발생하며, 기존 객체를 setState에 들어온 객체로 아예 교체해버린다. 병합시에는 병합하는 객체만 병합한다. 그 외는 무시되어 덮어씌워진다. 반면 깊은 병합은 기존의 상태를 지우지 않고, 전체 변경 안되고 오로지 변경된 부분만 교체된다. 

Data 는 한방향(부모에서 자식)으로만 전달 된다

부모 component나 자식 component 모두
어떤 특정 component가 stateful한지 stateless한지,
혹은 function component인지 class component인지 알 수 없다.

이것이 바로

state는 local하다 혹은 encapsulated하다 라고 말하는 이유이다.

component는 다른 component의 state에 접근하거나 출처를 알 수 없다.

유일하게 접근하는 방법이 있다. component는 자신의 state 하위 component에게 props로 전달할 수 있다.

<h2>It is {this.state.date.toLocaleTimeString()}.</h2>

또한 사용자 정의 component에게도 props로 전달할 수 있다.

<FormattedDate date={this.state.date} />

FormattedDate component는 date를 props로 받는데
이 date에 부모 component의 state 값이 할당되어 있으므로
접근할 수 있다. 하지만 FormattedDate가 state의 출처를 알 수는 없다.

function FormattedDate(props) {
  return <h2>It is {props.date.toLocaleTimeString()}.</h2>;
}

그저 props로 받아서 값에 접근할 수 있을 뿐이다.

이것을 Top-down data flow라고 부른다.

React는 아래와 같은 특징을 가진다.

  • 어떤 state는 항상 특정 component에게 소유되어져 있다.
  • 그 state로부터 나온 어떠한 data 혹은 UI들은 자식 component들에게만 영향을 줄 수 있다.

props를 부모 component에서 자식 component로 흐르는 폭포라고 가정하면 state는 중간 중간 component들로부터 추가되는 물줄기라고 할 수 있다.

모든 component들이 독립적으로 구성되어져 있다.
아래 App component를 보자.

function App() {
  return (
    <div>
      <Clock />
      <Clock />
      <Clock />
    </div>
  );
}

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

총 3개의 Clock이 있다.
각 Clock은 각자의 타이머가 설정되며 독립적으로 업데이트 된다.
React 앱에서 component가 stateful한지 혹은 stateless한지는 중요하지 않다. 왜냐하면 바뀔 수 있기 때문이다. 또한 Stateful한 component 내부에서 stateless한 component를 사용할 수 있으면 그 반대도 가능하다.

'개발 공부 > React' 카테고리의 다른 글

가장 쉬운 리액트 11강  (0) 2022.12.16
가장 쉬운 리액트 10강  (0) 2022.12.16
가장 쉬운 리액트 9강  (0) 2022.12.16
가장 쉬운 리액트 7~8강  (0) 2022.12.15
[TIL] virtual DOM  (0) 2022.12.15