리액트 입문(1장) 복습 및 정리

신동연·2021년 7월 19일
0

1. 리액트란?

리액트란 페이스북에서 만든 사용자 UI 구축을 위한 라이브러리입니다.

리액트는 처음엔 페이스북 핵심 기능인 뉴스피드 개발 목적으로 만들어졌고 나중에는 인스타그램에서도 리액트를 사용하게 되었습니다. 2013년도에는 페이스북 직원이 아니여도 쓸 수 있도록 오픈소스로 세상에 공개되었습니다.

+리액트는 어쩌다 만들어 졌을까?

기존의 HTML로 구성된 UI를 제어할 때는 DOM을 변형시키기 위해 브라우저의 DOM Selector API 를 사용해서 특정 DOM 을 선택한뒤, 특정 이벤트가 발생하면 변화를 주도록 설정해야합니다.

<h2 id="number">0</h2>
<div>
  <button id="increase">+1</button>
  <button id="decrease">-1</button>
</div>

위와 같이 HTML로 구성되어 있고, id 를 사용하여 각 DOM 을 선택한뒤, 원하는 이벤트가 발생하면 DOM 의 특정 속성을 바꾸어주어야 합니다.

JavaScript 개발자라면, 코드를 최대한 깔끔하게 정리하여 쉽게 유지보수를 할 수도 있겠지만, 대부분의 경우 웹 애플리케이션의 규모가 커지면, DOM 을 직접 건드리면서 작업을 하면 코드가 난잡해지기 쉽습니다.

이로 인해 Ember, Backbone, AngularJS 등의 프레임워크가 만들어졌었는데, 이 프레임워크들은 작동방식이 각각 다르지만, 쉽게 설명하자면 자바스크립트의 특정 값이 바뀌면 특정 DOM의 속성이 바뀌도록 연결을 해주어서, 업데이트 하는 작업을 간소화해주는 방식으로 웹개발의 어려움을 해결해주었습니다.

하지만 리액트의 경우에는 조금 다른 발상에서 만들어졌습니다. 리액트는 어떠한 상태가 바뀌었을때, 그 상태에 따라 DOM 을 어떻게 업데이트 할 지 규칙을 정하는 것이 아니라, 아예 다 날려버리고 처음부터 모든걸 새로 만들어서 보여준다면 어떨까? 라는 아이디어에서 개발이 시작되었습니다.

하지만, 정말로 동적인 UI 를 보여주기 위해서 모든걸 다 날려버리고 모든걸 새로 만들게 된다면, 속도가 굉장히 느려지게 됩니다.
작은 웹애플리케이션이라면 상관없겠지만 규모가 큰 웹애플리케이션이라면 상상도 할 수 없는 일이지요.

하지만, 리액트는 가상돔(Virtual DOM)이라는 것을 사용해서 이를 가능하게 하였습니다.

  • 가상돔이란?
    브라우저에 실제로 보여지는 DOM 이 아니라 그냥 메모리에 가상으로 존재하는 DOM 으로서 그냥 JavaScript 객체이기 때문에 작동 성능이 실제로 브라우저에서 DOM 을 보여주는 것 보다 속도가 훨씬 빠릅니다.
    이를 통해, "업데이트를 어떻게 할 지" 에 대한 고민을 하지 않으면서, 빠른 성능도 지켜낼 수 있게 되었습니다.

2. JSX

JSX 는 리액트에서 생김새를 정의할 때, 사용하는 문법입니다. 얼핏보면 HTML 같이 생겼지만 실제로는 JavaScript 입니다.

return <div>안녕하세요</div>;

리액트 컴포넌트 파일에서 XML 형태로 코드를 작성하면 babel 이 JSX 를 JavaScript 로 변환을 해줍니다.

Babel 은 자바스크립트의 문법을 확장해주는 도구입니다. 아직 지원되지 않는 최신 문법이나, 편의상 사용하거나 실험적인 자바스크립트 문법들을 정식 자바스크립트 형태로 변환해줌으로서 구형 브라우저같은 환경에서도 제대로 실행 할 수 있게 해주는 역할을 합니다.

JSX 가 JavaScript 로 제대로 변환이 되려면 지켜주어야 하는 몇가지 규칙이 있습니다.

  • 태그는 꼭 닫혀있어야 합니다.

  • 두개 이상의 태그는 무조건 하나의 태그로 감싸져 있어야 합니다.

  • 세번째로 JSX 내부에 자바스크립트 변수를 보여줘야 할 때에는 {}로 감싸서 보여줍니다.

  • 네번째로 css class를 사용할 때는 class가 아니라 className을 사용해야 합니다.

3. useState

리액트 16.8 이전 버전에서는 함수형 컴포넌트에서는 상태를 관리할 수 없었는데 리액트 16.8 에서 Hooks 라는 기능이 도입되면서 함수형 컴포넌트에서도 상태를 관리할 수 있게 되었습니다. useState 라는 함수가 바로 리액트의 Hooks 중 하나입니다.

useState는 클래스 컴포넌트의 state를 함수 컴포넌트에서 사용할 수 있게 해줍니다. useState 함수의 return 값은 배열이며, 1번째에는 현재의 변수 값이 2 번째에는 값을 갱신하는 함수가 할당된다. 컴포넌트 안에서 useState는 여러 번 사용할 수 있으며, 여러 State를 가질 수 있다.

  • Counter 컴포넌트 만들기

    Counter.js

import React from 'react';

function Counter() {
  return (
    <div>
      <h1>0</h1>
      <button>+1</button>
      <button>-1</button>
    </div>
  );
}

export default Counter;

그 다음엔 App 에서 Counter 를 렌더링 한다.

APP.js

import React from 'react';
import Counter from './Counter';

function App() {
  return (
    <Counter />
  );
}

export default App;

  • 이벤트 설정하기

이제, Counter 에서 버튼이 클릭되는 이벤트가 발생 했을 때, 특정 함수가 호출되도록 설정을 해본다.

Counter.js

import React from 'react';

function Counter() {
  const onIncrease = () => {
    console.log('+1')
  }
  const onDecrease = () => {
    console.log('-1');
  }
  return (
    <div>
      <h1>0</h1>
      <button onClick={onIncrease}>+1</button>
      <button onClick={onDecrease}>-1</button>
    </div>
  );
}

export default Counter;

  • 동적인 값 끼얹기, useState

컴포넌트에서 동적인 값을 상태(state)라고 부릅니다. 리액트에 useState 라는 함수를 사용하면 컴포넌트에서 상태를 관리 할 수 있다.

Counter.js

import React, { useState } from 'react';

function Counter() {
  const [number, setNumber] = useState(0);

  const onIncrease = () => {
    setNumber(number + 1);
  }

  const onDecrease = () => {
    setNumber(number - 1);
  }

  return (
    <div>
      <h1>{number}</h1>
      <button onClick={onIncrease}>+1</button>
      <button onClick={onDecrease}>-1</button>
    </div>
  );
}

export default Counter;
import React, { useState } from 'react';

이 코드는 리액트 패키지에서 useState 라는 함수를 불러온다.

const [number, setNumber] = useState(0);

useState 를 사용 할 때에는 상태의 기본값을 파라미터로 넣어서 호출해준다. 이 함수를 호출해주면 배열이 반환되는데, 여기서 첫번째 원소는 현재 상태, 두번째 원소는 Setter 함수이다.

 const onIncrease = () => {
    setNumber(number + 1);
  }

  const onDecrease = () => {
    setNumber(number - 1);
  }

Setter 함수는 파라미터로 전달 받은 값을 최신 상태로 설정해준다.

코드를 다 수정 하고 버튼들을 눌러보면 숫자가 잘 바뀌는 걸 볼 수 있다.

4. useRef

리액트를 사용하는 프로젝트에서도 가끔씩 DOM 을 직접 선택해야 하는 상황이 발생 할 때도 있습니다. 이럴 때 리액트에서 ref 라는 것을 사용합니다.

함수형 컴포넌트에서 ref 를 사용 할 때에는 useRef 라는 Hook 함수를 사용합니다.

const nameInput = useRef();

const onClick = () => {
    nameInput.current.focus();
}

return(
    <input ref={nameInput} />
    <button onClick={onClick}>클릭</button>
)

버튼을 클릭하면 인풋에 포커스가 잡힙니다.

useRef Hook 은 DOM 을 선택하는 용도 외에도, 컴포넌트 안에서 조회 및 수정 할 수 있는 변수를 관리하는 용도로도 있습니다.

useRef Hook 은 DOM 을 선택하는 용도 외에도, 다른 용도가 한가지 더 있는데요, 바로, 컴포넌트 안에서 조회 및 수정 할 수 있는 변수를 관리하는 것 입니다.

App.js

import React from 'react';

import UserList from './UserList';

function App() {
  const users = [
    {
      id: 1,
      username: 'velopert',
      email: 'public.velopert@gmail.com'
    },
    {
      id: 2,
      username: 'tester',
      email: 'tester@example.com'
    },
    {
      id: 3,
      username: 'liz',
      email: 'liz@example.com'
    }
  ];
  return <UserList users={users} />;
}

export default App;

UserList.js

import React from 'react';

function User({ user }) {
  return (
    <div>
      <b>{user.username}</b> <span>({user.email})</span>
    </div>
  );
}

function UserList({ users }) {
  return (
    <div>
      {users.map(user => (
        <User user={user} key={user.id} />
      ))}
    </div>
  );
}

export default UserList;

위에서 UserList 컴포넌트 내부에서 배열을 직접 선언해서 사용을 하고 있는데 이렇게 UserList 에서 선언해서 사용하는 대신에, 이 배열을 App 에서 선언하고 UserList 에게 props 로 전달을 해주겠습니다.

App 에서 useRef() 를 사용하여 nextId 라는 변수를 만듭니다.

App.js

import React, { useRef } from 'react';
import UserList from './UserList';

function App() {
  const users = [
    {
      id: 1,
      username: 'velopert',
      email: 'public.velopert@gmail.com'
    },
    {
      id: 2,
      username: 'tester',
      email: 'tester@example.com'
    },
    {
      id: 3,
      username: 'liz',
      email: 'liz@example.com'
    }
  ];

  const nextId = useRef(4);
  const onCreate = () => {
    // 나중에 구현 할 배열에 항목 추가하는 로직
    // ...

    nextId.current += 1;
  };
  return <UserList users={users} />;
}

export default App;

useRef() 를 사용 할 때 파라미터를 넣어주면, 이 값이 .current 값의 기본값이 됩니다. 그리고 이 값을 수정 할때에는 .current 값을 수정하면 되고 조회 할 때에는 .current 를 조회하면 됩니다.

5. useEffect

useEffect는 리액트 컴포넌트가 렌더링될 때마다 특정 작업을 수행하도록 설정할 수 있는 Hook입니다.

랜더링될 때마다 콘솔로그에 '렌더링이 완료되었습니다' 라고 뜨게 하는 코드 작성

info.js

결과 -> input에 입력될때마다 콘솔에 내용이 찍힌다.

마운트될 때만 실행하고 싶을 때
useEffect에서 설정한 함수를 컴포넌트가 화면에 맨 처음 렌더링될 때만 실행하고, 업데이트 될 때는 실행하지 않으려면 함수의 두 번째 피라미터로 비어 있는 배열을 넣어 주면 된다.

결과 -> 컴포넌트가 처음 나타날 때만 콘솔에 문구가 나타나고, 그 이후에는 나타나지 않는다.

특정 값이 업데이트될 때만 실행하고 싶을 때
useEffect의 두 번째 피라미터로 전달되는 배열 안에 검사하고 싶은 값을 넣어주면 된다. 배열 안에는 useState를 통해 관리하고 있는 상태를 넣어 주어도 되고, props로 전달받은 값을 넣어 주어도 된다.

6. useMemo

useMemo는 메모리제이션된 값을 반환합니다. 하위 컴포넌트가 상위 컴포넌트로부터 a,b라는 두개의 props를 전달받는다고 가정하면. 이를 전달받은 하위컴포넌트는 a와 b를 각각 핸들링하여 새로운 값으로 변환해서 렌더링하는 역할을 한다.

하위 컴포넌트는 props중 하나만 변경되어도 전체가 렌더링 되는데 이때, a값만 바뀌었다한들 b값역시 재계산하여 다시 렌더링됩니다.

App.js

import Info from "./Info";

const App = () => {
  const [color, setColor] = useState("");
  const [movie, setMovie] = useState("");

  const onChangeHandler = e => {
    if (e.target.id === "color") setColor(e.target.value);
    else setMovie(e.target.value);
  };

  return (
    <div className="App">
      <div>
        <label>
          What is your favorite color of rainbow ?
          <input id="color" value={color} onChange={onChangeHandler} />
        </label>
      </div>
      <div>
        What is your favorite movie among these ?
        <label>
          <input
            type="radio"
            name="movie"
            value="Marriage Story"
            onChange={onChangeHandler}
          />
          Marriage Story
        </label>
        <label>
          <input
            type="radio"
            name="movie"
            value="The Fast And The Furious"
            onChange={onChangeHandler}
          />
          The Fast And The Furious
        </label>
        <label>
          <input
            type="radio"
            name="movie"
            value="Avengers"
            onChange={onChangeHandler}
          />
          Avengers
        </label>
      </div>
      <Info color={color} movie={movie} />
    </div>
  );
};

export default App;

Info.js

const getColorKor = color => {
    console.log("getColorKor");
    switch (color) {
      case "red":
        return "빨강";
      case "orange":
        return "주황";
      case "yellow":
        return "노랑";
      case "green":
        return "초록";
      case "blue":
        return "파랑";
      case "navy":
        return "남";
      case "purple":
        return "보라";
      default:
        return "레인보우";
    }
  };

  const getMovieGenreKor = movie => {
    console.log("getMovieGenreKor");
    switch (movie) {
      case "Marriage Story":
        return "드라마";
      case "The Fast And The Furious":
        return "액션";
      case "Avengers":
        return "슈퍼히어로";
      default:
        return "아직 잘 모름";
    }
  };


const Info = ({ color, movie }) => {
  const colorKor = getColorKor(color);
  const movieGenreKor = getMovieGenreKor(movie);

  return (
    <div className="info-wrapper">
      제가 가장 좋아하는 색은 {colorKor} 이고, <br />
      즐겨보는 영화 장르는 {movieGenreKor} 입니다.
    </div>
  );
};

이 경우 App컴포넌트의 입력창에서 color값을 변경할때 getMovieGenreKor가 호출되게 됩니다. 즉 color값만 바꾸어도 상관없는 함수가 재호출된다는 것입니다.

useMemo를 사용해서 colorKor과 movieGenreKor를 아래와같이 바꿔보면

import React, { useMemo } from "react";

const colorKor = useMemo(() => getColorKor(color), [color]);
const movieGenreKor = useMemo(() => getMovieGenreKor(movie), [movie]);

useMemo를 사용하면 의존성 배열에 넘겨준 값이 변경되었을 경우만 다시 계산한다. 함수가 복잡한 경우에는 성능상의 차이가 확연합니다.

7. useCallBack

useCallBack은 메모리제이션 된 함수를 반환한다. useMemo는 값을 반환했지만, useCallBack은 함수를 리턴하는 것입니다.

위 예제에서 App.js에서 useCallback을 임포트하고 onChangeHandler함수의 선언부를 아래와같이 바꾸어보면

App.js

import React, { useState, useCallback } from "react";

const onChangeHandler = useCallback(e => {
    if (e.target.id === "color") setColor(e.target.value);
    else setMovie(e.target.value);
  }, []);

이렇게 useCallBack을 사용하는 경우 불필요한 재정의를 최소화하고 최적화 할 수 있습니다.

  • useMemo를 사용하기 가장 좋은 케이스는 동일한 props로 자주 렌더링 될 때입니다. 예를들어 특정 앨범리스트를 렌더링하거나 잘바뀌지 않는 데이터를 서버로부터 받아와 렌더링하는 경우입니다.

  • 다음으로 props로 하위 컴포넌트에 Callback을 넘기는 경우는 useCallback으로 함수를 선언하는 것이 바람직합니다. 함수가 상위 컴포넌트에서 매번 재선언되어 하위컴포넌트에서 리렌더링하는 것을 최소화 할 수 있기 때문입니다.

8. useReducer

우리가 이전에 만든 사용자 리스트 기능에서의 주요 상태 업데이트 로직은 App 컴포넌트 내부에서 이루어졌었습니다. 상태를 업데이트 할 때에는 useState 를 사용해서 새로운 상태를 설정해주었는데, 상태를 관리하게 될 때 useState 를 사용하는것 말고도 다른 방법이 있습니다.

바로, useReducer 를 사용하는건데, 이 Hook 함수를 사용하면 컴포넌트의 상태 업데이트 로직을 컴포넌트에서 분리시킬 수 있습니다. 상태 업데이트 로직을 컴포넌트 바깥에 작성 할 수도 있고, 심지어 다른 파일에 작성 후 불러와서 사용 할 수도 있습니다.

+reducer 는 현재 상태와 액션 객체를 파라미터로 받아와서 새로운 상태를 반환해주는 함수입니다.

function reducer(state, action) {
  // 새로운 상태를 만드는 로직
  // const nextState = ...
  return nextState;
}

reducer 에서 반환하는 상태는 곧 컴포넌트가 지닐 새로운 상태가 됩니다.

action 은 업데이트를 위한 정보를 가지고 있습니다. 주로 type 값을 지닌 객체 형태로 사용하지만, 꼭 따라야 할 규칙은 따로 없습니다.

useReducer 사용법

const [state, dispatch] = useReducer(reducer, initialState);

여기서 state 는 우리가 앞으로 컴포넌트에서 사용 할 수 있는 상태를 가르키게 되고, dispatch 는 액션을 발생시키는 함수라고 이해하면 됩니다. 이 함수는 다음과 같이 사용합니다: dispatch({ type: 'INCREMENT' }).

그리고 useReducer 에 넣는 첫번째 파라미터는 reducer 함수이고, 두번째 파라미터는 초기 상태입니다.

// 1. reducer 함수 만들기
function reducer(state, action) {
    switch (action.type) {
        case 'INCREMENT':
            return state + 1;
        case 'DECREMENT':
            return state - 1;
        default:
            //return state
            throw new Error('unhandled action');
        // return state를 할 경우에는 준비하는 액션이 들어와도 별 액션이 안생기고,
        // error 를 던져주면 콘솔에서 어떤 에러가 발생햇다는걸 볼수있음
    }
}

function Counter() {
    // 2. useReducer 사용
    const [number, dispatch] = useReducer(reducer, 0);
    // [현재상태, dispatch] = useReducer(reducer함수, 초기값);

    const onIncrease = () => {
        dispatch({
            type: 'INCREMENT'
        });
    }
    const onDecrease = () => {
        dispatch({
            type: 'DECREMENT'
        });
    }

어떨 때 useReducer를 쓰고 어떨 때 usestate를 쓸까?

정해진 답은 없다고 합니다. 컴포넌트에서 관리하는 값이 딱 하나이고, 그 값이 단순 숫자, 문자열, 불리언 값이라면 useState로 관리하는게 편합니다.
하지만, 관리하는 값이 여러개여서 상태의 구조가 복잡해지거나 수정,추가,삭제 등을 해야할 때는 useReducer가 편할 수 있습니다.
즉, 간단한 거면 useState, 복잡한거면 useReduce 를 쓰면 됩니다.

위 내용은 "1장 리액트 입문"을 통해 참고하여 공부한 내용입니다.

0개의 댓글