React: JSX, props and state, event handling

JSX


JSX 소개 – React
Babel

babel이 html 문법으로 작성한 형태를 React createElement 형태로 변환시켜준다. 즉, 어렵게 작성하지 않고 html 형태로 작성해도 된다.

const element = (
  <div>
    <h1>Hello!</h1>
    <h2>Good to see you here.</h2>
		<h1 className="greeting">
    Hello, world!
	  </h1>
  </div>
);
  • JSX 사용 이유

    React.createElement 보다 가독성이 훨씬 좋다.
    babel과 같은 컴파일 과정에서 문법적 오류를 인지하기가 쉽다.

  • JSX 규칙

    최상위 요소가 하나여야 한다. (최상위 요소가 두개 존재하면 error)
    만약! 같은 최상위 요소를 두개 만들고 싶다면? Fragment를 사용한다.
    <></> 태그를 최상위 요소로 사용하는 것이다.

    중괄호{}를 사용하여 자바스크립트 표현식을 사용한다. (중괄호 안에 문자열, 변수 등)

    if문은 사용할 수 없다.
    삼항 연산자 혹은 &&를 사용한다.

    class 대신에 className을 사용해서 class를 적용한다.

    자식요소가 있다면 꼭 닫아야 하며, 자식요소가 없으면 열면서 닫아야 한다.

Props와 State


  • props

    컴포넌트 외부에서 컴포넌트에게 주는 데이터이다.

    class Component extends React.Component {
      render() {
        return (
        <div>{this.props.message}</div>
        )
      }
    }
      
    ReactDOM.render(
      <Component message="Hello" />
    );
    
  • state

    컴포넌트 내부에서 변경할 수 있는 데이터이다.

    class Component extends React.Component {
      state = {
        count: 0
      }
      
      render() {
        return (
        <div>{this.state.count}</div>
        )
      }
    }
    

두가지 모두 변경이 발생하면 render가 다시 일어날 수 있다. 즉, 컴포넌트는 다시 그림을 그리려고 한다.

  • render()

    props와 state를 바탕으로 컴포넌트를 그린다.
    props와 state가 변경되면 컴포넌트를 다시 그린다. 컴포넌트를 그리는 방법을 기술하는 함수가 랜더함수이다.

    • React는 Virtual DOM을 사용해서 성능을 개선했다. DOM을 직접 조작하지 않고, 가상 돔 트리를 사용하여 변경된 부분만 바꿔준다.
    • CSR은 모든 JS 파일을 다운로드 받기 전까지 화면에 아무것도 출력하지 않는다. SSR은 JS 파일을 다운로드 받지 않아도 화면에 보이지만, 유저가 사용하지 못하다가 JS 파일을 모두 다운로드 받으면 사용 가능해진다.
    • React 컴포넌트는 함수로 이루어져 있으며, 인자로 props가 들어오고, 함수 안에서는 반드시 return을 해주어야 한다. React 컴포넌트는 React Element를 return해야 한다.
    • React는 Virtual DOM을 사용해서 성능을 개선했다. DOM을 직접 조작하지 않고, 가상 돔 트리를 사용하여 변경된 부분만 바꿔준다.
    • CSR은 모든 JS 파일을 다운로드 받기 전까지 화면에 아무것도 출력하지 않는다. SSR은 JS 파일을 다운로드 받지 않아도 화면에 보이지만, 유저가 사용하지 못하다가 JS 파일을 모두 다운로드 받으면 사용 가능해진다.
    • React 컴포넌트는 함수로 이루어져 있으며, 인자로 props가 들어오고, 함수 안에서는 반드시 return을 해주어야 한다. React 컴포넌트는 React Element를 return해야 한다.

Event Handling


HTML DOM에 클릭하면 이벤트가 발생하고, 그에 맞는 변경이 일어나도록 한다.

Event Handling은 camelCase로만 사용할 수 있다. (onClick, onMouseEnter)
이벤트에 연결된 자바스크립트 코드는 함수이다. (이벤트={함수}와 같이 쓴다.)
실제 DOM 요소들에만 사용이 가능하다. (리액트 컴포넌트에 사용하면, 그냥 props로 전달된다.)

function Component() {
	return (
		<button
			onClick={() => {
				console.log("clicked")
			}}
		>
			click!
		</button>
	)
}

React: React Component, React.createElement

React Component


  • Class 컴포넌트

    class 컴포넌트는 꼭 render를 해줘야 한다.

    import React from 'react';
      
    // 정의
    class ClassComponent extends React.Component {
      render() {
        return (<div>Hello</div>)
      }
    }
      
    // 사용
    <ClassComponent />
    
  • Function 컴포넌트

    import React from 'react';
      
    // 정의1
    function FunctionComponent() {
      return <div>Hello</div>
    }
      
    // 정의2
    const FunctionComponent = () => <div>Hello</div>
      
    // 사용
    <FunctionComponent />
    

React.createElement


React.createElement(
	type,
	[props],
	[...children]
);
  • type

    태그 이름 (문자열), 기존에 작성한 리액트 컴포넌트, React.Fragment

    • 태그 이름 (문자열) type

      ReactDOM.render(
        React.createElement("h1", null, `type이 "태그 이름 문자열"입니다.`)
      )
      

      type이 h1이고 데이터를 주입하지 않으며 자식으로는 해당 문자열(type이 블라블라)이 들어가는 react element를 만들었다.

      <h1>type이 “태그 이름 문자열”입니다.</h1>

    • 리액트 컴포넌트 type

      const Component = () => {
        return React.createElement(
          "p",
          null,
          `type이 "리액트 컴포넌트" 입니다.`
        )
      }
          
      ReactDOM.render(React.createElement(Component))
      // ReactDOM.render(React.createElement(Component,null, null))
      

      <p>type이 “리액트 컴포넌트”입니다.</p>

    • React.Fragment

      ReactDOM.render(
        React.createElement(
          React.Fragment,
          null,
          `type이 "React.Fragment"입니다.`,
          `type이 "React.Fragment"입니다.`,
          `type이 "React.Fragment"입니다.`
        )
      )
      

      React.Fragment는 DOM에 별도의 노드를 추가하지 않고 여러 자식을 그룹화할 수 있다.

      Fragments – React

  • props

    리액트 컴포넌트에 넣어주는 데이터 객체

  • children

    자식으로 넣어주는 요소들

React: 시작하기

React Concept


  • Component

    img, div같이 이미 정해진 태그 이름이 아닌, 내가 직접 만든 태그이다. 즉, 내가 정의한 컴포넌트이다!

    src, class, name, props는 밖에서 넣어주는 데이터이다.
    html, css, js를 합쳐서 내가 정의한 이름으로 정의하여 남들도 사용할 수 있고 나도 재사용이 가능한 태그(같은 것)이다.

  • Virtual DOM

    react는 DOM을 직접 제어하지 않는다.
    가상의 돔 트리를 사용해서, 이전 상태와 이후 상태를 비교하여 바뀐 부분을 찾아 자동으로 바꿔준다.

    달라진 부분을 찾아서 diff로 달라진 부분과 해당 하위를 달라진 내용으로 변경해준다.

  • CSR (Client Side Randering)

    js가 전부 다운로드 되어 react 앱이 정상 실행되기 전까지 화면에 보이지 않는다. js가 전부 다운되어 react 앱이 정상 실행된 후, 화면이 출력되며 유저가 인터랙션이 가능해진다.

    html 다운 → js 다운 → js 안에서 react 앱을 실행 (아직 화면에 보이는 것 없음) → react 내부의 컴포넌트가 화면에 출력 → user에게 보임

  • SSR (Server Side Randering)

    js가 전부 다운되지 않아도 화면에 보인다. 하지만, 유저가 사용할 수는 없다. js가 전부 다운되어 react 앱이 정상 실행되면 유저가 사용이 가능하다.

    html 다운 → js 다운(과 동시에 이미 랜더링 된 html 파일이 화면에 보임, 단 동작은 되지 않음) → react 앱 실행 (js까지 다운된 화면 보임) → react 화면 출력 (완벽한 웹 사용 가능)

React 라이브러리

  • React 핵심 모듈

    // 1
    import ReactDom from 'react-dom';
    // 2
    import React from 'react';
    
    1. ReactDom은 리액트 컴포넌트로 HTMLElemnet와 연결한다.
    2. React는 리액트 컴포넌트를 만든다.
  • react 시작

    # npm 프로젝트 만들기
    npm init -y
      
    # 파일 서버처럼 관리
    npx serve
    
  • react CDN

    CDN 링크: React

  • react로 render

    바닐라 js는 값들이 변경될 때마다, render를 해줘야 한다.
    처음 초기화 할때와 값이 바뀔때마다 계속 render를 해줘야 하는데, react는 이걸 좀 더 간편하게 해준다.

    const Rㅣ액트컴포넌트이름 = props => {
      return React.creatElement(
        규정할 , 넣어줄 , 
        태그 안에 들어갈 
      );
      // 예시
      return React.creatElement(
        'p', 
        null, 
        `${props.message} : ${props.count}`
      );
    }
      
    ReactDOM.render(리액트 컴포넌트, 실제로 그려질 )
    // 예시
    ReactDOM.render(
      React.createElement(Component, {message: "init", count: "0"}, null),	
      document.querySelector('#root')
    );
    

    리액트 컴포넌트는 기본적으로 함수라고 생각하면 된다.
    인자로 props가 들어오고, 함수 안에서 꼭 return을 해줘야 한다. return을 할때, react의 element로 return이 돼야한다.
    react element는 실제 dom element가 아니고 react가 규정하는 방식으로 만들어진 element를 의미한다.

E2E Test: Cypress

  • E2E 테스트

    단위 테스트와는 달리 어떤 로직을 검증하는 것이 아닌, 실제 화면이 원하는 시나리오대로 정상적으로 동작하는지 확인하는 용도의 테스트이다.

  • Cypress 설치

    JavaScript Web Testing and Component Testing Framework: cypress.io

    npm i cypress eslint-plugin-cypress
    

    cypress가 eslint에서 문법 에러가 발생하지 않도록 eslint plugin도 설치해준다.

    • package.json

      "scripts": {
        "test:e2e": "cypress open"
      }
      
    • .eslintrc.js

      env: {
        ...
        'cypress/globals': true
      },
      plugin: [
        'cypress'
      ]
      
  • Cypress API

    Cypress Documentation: API

  • Headless

    cypress의 프로그램이나 브라우저가 열리지 않는 터미널에서만 동작하도록 설정

    "scripts": {
      "test:e2e": "cypress open",
      "test:e2e:headless" : "cypress run"
    }
    

    터미널에서만 테스트가 진행되면, 테스트가 동작하는 화면을 동영상으로 얻을 수 있다. 해당 테스트를 실행하면 videos 폴더가 생성되면서 테스트가 동작한 동영상이 생기게 된다.

    버전 관리를 할때 비디오를 깃허브에 같이 올리는 것은 그닥 좋은 방법이 아니다. 따라서, cypress의 videos 파일은 버전관리를 하지 않는다.

    cypress/videos
    
  • E2E 테스트와 단위 테스트 충돌 방지

    jest.config.js 파일에 해당 내용을 추가한다.

    단위 테스트 부분에서 cypress 폴더안에 있는 테스트 파일들을 사용하지 않도록 만들어준다.

    modulePathIgnorePatterns: [
      ...,
      '<rootDir>/cypress'
    ]
    

Unit Test: 모의(Mock)함수

모의(Mock) 함수


테스트에 방해되는 것(소요시간 등)을 가짜의 개념으로 만들어서 테스트를 진행하는 함수를 모의함수라고 한다.

모의로, 임시로 테스트를 하기 위해 모의함수를 만들어서 테스트를 진행한다.
테스트가 잘 되는지 단순하게 확인할 때, 모든 시간을 기다리는 것은 비효율적이기 때문에 모의함수를 만들어서 실제 걸리는 시간을 단축하여 테스트가 올바른지만 확인한다.

Mocking: 실제 함수를 가짜 함수로 만들어서 원하는 데이터만 반환하도록 만들어주는 행위

import * as example from './example';

describe('비동기 테스트', () => {
  test('async/await', async () => {
    jest.spyOn(example, 'asyncFn')
      .mockResolvedValue('Done?')
    const res = await example.asyncFn()
    expect(res).toBe('Done!')
  },7000)
})

asyncFn 비동기 함수를 모의함수로 만들었다. mockResolvedValue를 통해서 비동기로 실행이 되면, 문자데이터를 반환하도록 설계했다.
example이라는 객체데이터 내부에서 asyncFn 함수를 실행했을 때, 실제로 반환되는 값이 “Done?” 문자 데이터이다. 테스트 코드를 통해서 “Done!”와 일치하는지 테스트를 하면 일치하지 않기 때문에 실패한다.

example.js 파일에서 asyncFn이 실행되는데 6초가 걸리지만, 해당 시간을 무시하고 내가 원하는 특정 값을 반환하게 가짜로 함수(모의함수)를 만들어서 테스트 로직에서 동작시킨다.

기존에는 7초정도의 시간이 소요됐지만, 모의 함수 생성으로 인해 실제 테스트 시간은 2초가 걸리지 않았다.

  • 쓸만한(?) 로직

    import axios from '../node_modules/axios/lib/axios.js';
    import _upperFirst from 'lodash/upperFirst'
    import _toLower from 'lodash/toLower'
      
    export async function fetchMovieTitle() {
      const res = await axios.get('https://omdbapi.com?apikey=7035c60c&i=tt4520988')
      return _upperFirst(_toLower(res.data.Title)) // Frozen ii
    }
    
    import axios from 'axios';
    import { fetchMovieTitle } from './example';
      
    describe('비동기 테스트', () => {
      test('영화 제목 반환', async () => {
        axios.get = jest.fn(() => {
          return new Promise(resolve => {
            resolve({
              data: {
                Title: 'Frozen II'
              }
            })
          })
        })
        const title = await fetchMovieTitle()
        expect(title).toBe('Frozen ii')
      })
    })
    

    해당 코드에서는 모의함수를 작성하지 않아도 테스트가 잘 작동한다. 그렇다면 왜 모의함수를 만들어 준 걸까?

    이유는 외부 요인으로 인해 테스트가 불가한 상황을 피하기 위해서이다.
    axios로 omdbAPI를 불러오는데, 인터넷 연결이 끊기게 된다면 해당 테스트는 통과임에도 실패하게 된다.

    모의함수로 데이터를 임의로 만들어주어 테스트함으로, 외부 요인으로 인해 테스트가 불가해지는 상황을 막을 수 있다.