본문 바로가기
개발공부일지/React

React - React 기본 사용법 및 데이터 처리하기

by Hynn1429 2023. 8. 10.
반응형

안녕하세요.

Hynn 입니다.

 

이번 포스팅에서는 React 의 기본적 사용법을 되돌아 보는 시간을 가져보려고 합니다.

크게 4개의 대 주제로 사용해보도록 하겠습니다.

 

 

1. 데이터 입력 및 리스트 처리하기(Array)

 

1) 데이터 입력하기 

먼저 첫번째로 데이터 입력입니다.

일기장형태로 작성을 위해, 간단한 코드를 구현해보겠습니다.

 

이전 포스팅에서도 언급했듯이, 데이터를 입력하여 상태로 처리하는 것은 "useState"를 사용해 처리합니다.

이를 이제 Input 에서 값을 받아서 useState에 반영해야 합니다.

이를 한가지 구성을 알아보면 아래와 같이 코드를 작성하고, Input 에 값을 입력하면 입력되지 않는 것을 볼 수 있습니다.

import { useState } from 'react';

const DiaryEditor = () => {
  const [writer, setWriter] = useState('');
  return (
    <>
      <div className="DiaryEditor">
        <h2>Today Diary</h2>
        <div>
          <input value={writer} />
        </div>
      </div>
    </>
  );
};

export default DiaryEditor;

위와 같이 입력하면 Input 의 값이 변화하지 않을 것입니다.

이는, Input 의 초기값이 "" 빈 텍스트이고, 상태변화를 처리하는 setWriter 가 아닌 writer를 사용하고 있기 때문입니다.

이를 위해 사용하는 것은 바로 "onChange" 입니다.

 

단어 그대로 onChange 는 값이 바뀔때마다, 동작을 처리하는 것입니다.

이는 Callback 함수로 처리되게 되고, 매개변수로 event 혹은 e 로 보통 받게합니다. 이는 이전의 JavaScript 에서 사용한 event 와 동일한 의미입니다.

 

이를 작성하면 아래와 같이 작성할 수 있습니다.

 

import { useState } from 'react';

const DiaryEditor = () => {
  const [writer, setWriter] = useState('Hynn');
  return (
    <>
      <div className="DiaryEditor">
        <h2>Today Diary</h2>
        <div>
          <input
            value={writer}
            onChange={(e) => {
              console.log(e);
            }}
          />
        </div>
      </div>
    </>
  );
};

export default DiaryEditor;

즉, callback 함수의 형태로 작성하고, 매개변수로 e를 받고, console.log 로 출력하여 테스트를 해보면 아래와 같은 객체형태를 볼 수 있습니다.

SyntheticBaseEvent {_reactName: 'onChange', _targetInst: null, type: 'change', nativeEvent: InputEvent, target: input, …}
bubbles
: 
true
cancelable
: 
false
currentTarget
: 
null
defaultPrevented
: 
false
eventPhase
: 
3
isDefaultPrevented
: 
ƒ functionThatReturnsFalse()
isPropagationStopped
: 
ƒ functionThatReturnsFalse()
isTrusted
: 
true
nativeEvent
: 
InputEvent {isTrusted: true, data: '1', isComposing: false, inputType: 'insertText', dataTransfer: null, …}
target
: 
input
timeStamp
: 
368588.5

이 중에서, 우리가 필요한 것은 JavaScript 에서 사용했던, event 객체 내의 target, target 객체 내의 value 입니다.

이를 상태변화를 처리하는 setWriter 를 활용하면 아래와 같이 작성하여, 값을 처리할 수 있습니다.

import { useState } from 'react';

const DiaryEditor = () => {
  const [writer, setWriter] = useState('');
  return (
    <>
      <div className="DiaryEditor">
        <h2>Today Diary</h2>
        <div>
          <input
            value={writer}
            onChange={(e) => {
              setWriter(e.target.value);
            }}
          />
        </div>
      </div>
    </>
  );
};

export default DiaryEditor;

이제 이렇게 하면, Input에서 받은 값을 사용하여 State에서 이를 처리할 수 있도록 구현이 됩니다.

이를 보다 효율적으로 작성하면 아래와 같이 작성할 수도 있습니다.

import { useState } from 'react';

const DiaryEditor = () => {
  const [state, setState] = useState({ writer: '', content: '' });

  return (
    <>
      <div className="DiaryEditor">
        <h2>Today Diary</h2>
        <div>
          <input
            name="writer"
            value={state.writer}
            onChange={(e) => {
              setState({ ...state, writer: e.target.value });
            }}
          />
        </div>
        <div>
          <textarea
            name="content"
            value={state.content}
            onChange={(e) => {
              setState({ ...state, content: e.target.value });
            }}
          />
        </div>
      </div>
    </>
  );
};

export default DiaryEditor;

먼저 상태를 state, setState 로 통일하고, useState 내에 객체형태로 각 Properties 를 구현합니다.

여기서는 기존에 사용한 writer, content 가 존재합니다.

이제 이를 사용하기 위해서는 점 표기법으로 "state.xxx" 으로 구현할 수 있습니다.

 

여기서 setState 사용시에, 주의해야 할 점은, 이렇게 state의 객체를 구현 후, 상태를 업데이트 할때는 항상 객체 전체 상태를 설정해야 합니다.

그렇지 않으면 다른 State 가 의도치 않게 처리될 수 있기 때문입니다.

 

위의 예제에서는 스프레드연산자를 사용하여 state 의 속성을 가지고 새로운 객체에 그대로 전달하고, 필요한 상태만 업데이트를 하도록 구현합니다. 이렇게 하면, 기존 상태는 그대로 가져오므로, 문제가 발생하지 않습니다. 

간단한 데이터 입력을 구현해보았습니다.

 

여기서, 이 코드를 조금 더 간결하게 구현하면 아래와 같이 구현할 수도 있습니다.

import { useState } from 'react';

const DiaryEditor = () => {
  const [state, setState] = useState({ writer: '', content: ''});

  const handleSubmit = (e) => {
    e.preventDefault();
    setState({
      ...state,
      [e.target.name]: e.target.value,
    });
  };

  return (
    <>
      <div className="DiaryEditor">
        <h2>Today Diary</h2>
        <div>
          <input name="writer" value={state.writer} onChange={handleSubmit} />
        </div>
        <div>
          <textarea
            name="content"
            value={state.content}
            onChange={handleSubmit}
          />
        </div>
      </div>
    </>
  );
};

export default DiaryEditor;

먼저 각 항목에 name 속성을 부여하고, 각 이름을 부여합니다.

이렇게 되면, 각Element 는 e.target 내에 name 이라는 속성이 추가로 부여됩니다. 따라서, 별도의 onChange 함수를 작성하고, setState 에, e.target.name 에 따라 , value 를 업데이트 하도록 처리하는 방식으로 구현할 수 있습니다.

이렇게 하면, 보다 더 간결하게 코드 작성이 가능합니다.  

 

 이제 이 기본적인 구현은 마쳤으니, 이를 이용해서 DOM 조작을 살펴보도록 하겠습니다.

여기서부터는 React 에서 제공하는, useState 와 같은 다양한 기능을 사용해보도록 하겠습니다.

 

첫번째 사용할 기능은 "useRef" 입니다.

useRef는 React 컴포넌트 내에서 DOM 요소에 접근하거나 변하지 않는 값을 저장하기 위해 사용됩니다. 

useRef를 사용하면 React.MutableRefObject 타입의 객체를 반환합니다. 

이 객체는 current라는 프로퍼티를 가지며, 이는 실제 DOM 요소에 대한 참조나 변하지 않는 값을 저장할 때 사용됩니다.

즉, useRef는 실제 DOM 요소에 대한 참조를 얻기 위한 훅으로 생각할 수 있으며, "useReference"의 약어로 이해할 수 있습니다.

따라서, ref.current.focus를 사용하면 useRef로 연결된 실제 DOM 요소인 writer나 content에 접근하고, 해당 요소를 포커스하는 것이 가능합니다.

 

 

2) 데이터 저장하기

 

이제, 데이터를 저장하는 기본 기능을 구현해보도록 하겠습니다.

이를 활용하려면 Array 를 주로 사용합니다.  실제 React 뿐 아니라, 많은 서비스에서도 데이터를 Array 내의 객체 형태로 많이들 사용합니다.

이를 구현하면서, 몇가지 기능을 활용하면서 처리해보도록 하겠습니다.

사실 기본적으로 이를 CRUD, 즉 Create, Read, Update, Delete 라는 4가지 기본 패턴을 뜻합니다. 이를 웹 서비스에서 수행하는 방법 중 잘 알려진 대표적인 API 가 RESTful API 의 작업으로도 이해하면 보다 쉽게 접근할 수 있습니다. 사실 CRUD 는 이곳에서만 사용되는 개념은 아니기 때문입니다.

 

즉 RESTful API 의 대응은 아래의 method 에 대응된다고 이해하시면 좋습니다.

 

  • Create - Post
  • Read - Get
  • Update - Put
  • Delete - Delete 

이 데이터 흐름들을 이해하기 위해서는, 단방향 데이터 흐름을 직접 그림으로 구현해보는것도 좋습니다.

제 부족한 그림으로 그린다면, 아래와 같이 그리게 됩니다. 

 

 

 

 

 

2. React Lifecycle & API

 

React 에서는 생명주기라는 것이 존재합니다. 

사람에게도 생애주기라는게 존재하듯이, React 에서도 이를 생명주기를 적용하여 사용합니다.

사람과 마찬가지로 Application 이, 실행되고 종료되는 과정까지의 흐름을 Lifecycle 이 존재합니다. 물론 사람처럼 어린아이부터 노년을 뜻하지는 않겠지만요.

 

크게 3개의 단계로 구분할 수 있습니다.

 

  • Mounting
  • Updating
  • Unmounting

즉, 단어 그대로, React 의 형태를 빌려서 설명한다면, 먼저 Component 를 Mounting 합니다.

React 에서 Component 를 마운트(끼우다) 하는것을 시작으로 생명주기가 시작됩니다. 

여기서는  일반적으로 "componentDidMount" 라는 단어로도 설명합니다.

 

그리고 나서, Mount 된 Component 에 Render, Props, setState, forceUpdate 와 같은 상태업데이트, Props 전달등을 통해 DOM 업데이트 및 Ref 사용등을 통해 데이터를 처리/업데이트하게 됩니다.

이를 "componentDidUpdate" 라고 합니다.

 

마지막으로 종료함으로써 마운트된 Component 를 해제합니다.

즉 이를 "componentWillUnmount" 라고 하여, 생명주기의 1 사이클이 종료됩니다.

 

 

하지만 위의 설명은 일반적으로 Class 기반의 컴포넌트에서 설명하는 구조입니다.

이번에 소개할 React 의 기능으로 함수형 컴포넌트에서도 생명주기와 유사한 기능을 제공합니다.

바로 이전에 사용했던 useState, useRef를 비롯하여, 다양한 Hook이 이러한 역활을 대신합니다.

 

여기서 Lifecycle 을 대신하는 역활인 useEffect 에 대해서도 알아보도록 하겠습니다.

Class component 의 고질적인 문제로 지적되는, 코드가 길어지는 문제가 존재합니다. 또한 결정적으로 "중복 코드"를 불가피하게 많이 작성하게 됩니다. 당연히 코드가 길어지면 가독성 문제등이 따라옵니다.

 

따라서 최근에는 함수형 컴포넌트가 대세가 된 이유기도 합니다.

 

기본적으로 React에서 useEffect 의 기본동작은 아래의 형태로 작성합니다. 

import React, { useEffect } from 'react';

useEffect(() => {
  //Callback Function
  return () => {
    // Cleanup Function
  };
}, []);

이 useEffect 의 기본적인 작성은, 실제 {} 내에 callback 함수를 작성하고, 마지막에 "[]" 에 어떠한 것을 작성하는지에 따라서 useEffect 의 동작이 결정됩니다.

여기서 빈 배열로 전달하면, Component 가 마운트 될때, 한번만 실행됩니다.

하지만 빈배열을 전달하지 않고, 아예 생략을 해버리면, 랜더링떄 마다, useEffect 가 실행됩니다.  하지만 배열에 특정한 상태등을 배열로 전달하면, 배열내의 있는 값이 변화할 대마다, Callback 함수가 실행됩니다. 

 

그리고 return 에는 클린업함수라고 하여, 이벤트 리스너나, 타이머를 정리하는 등의 작업을 수행합니다. 

이것이 React 에서 useEffect 의 기본적인 사용방법입니다.

 

또한 useEffect 를 사용하면 React 에서 컴포넌트가 마운트 되는 시점에 API 를 호출하고, API의 결과값을 초기값으로 사용할 수도 있습니다. 이는 axios, fetch 와 같은 요소를 사용해서 처리할 수 있습니다. 

다만 fetch의 경우 기본 API로서, 간단한 작업을 처리할때는 fetch 가 적합하고, 많은 것을 처리할때는 간단하게 axios 라이브러리를 추가로 설치하여 손쉽게 처리할 수 있습니다.

 

이는 이후에 실제 프로젝트 코드에 자주등장하기때문에, 예제 코드는 생략하겠습니다.

 

 

3. React 성능 최적화 하기

 

 React 성능 최적화를 위해 사용되는 몇가지 기능을 소개하려고 합니다.

각 기능의 대한 기본적인 설명 및 기초 사용법만을 작성해두었습니다.

실제 환경에 따라 사용하는 형태가 달라질 수 있기 때문에, 각 프로젝트에서 최적화에 대한 고민이 필요할 때 사용하는게 좋겠습니다.

 

1) useMemo

useMemo 는 Memoization 기법에 기인한 기능으로 이해하시는게 좋습니다.

즉, 단어 그대로, 연산 결과를 기억해두었다가, 동일한 계산을 사용자가 요청하면, 기억해둔 답을 바로 제출하여 효율성을 올리는 기법입니다.

이를 위해서는, 시간이 되실때 피보나치 수열을 코드로 구현해보시는것을 권합니다.

 

예시코드를 바로 작성해서 설명해드리도록 하겠습니다

import React, { useState, useMemo } from 'react';

const HynnCompoentn = ({ item }) => {
  const [keyword, setKeyword] = useState('');
  const [sort, setSort] = useState('asc');

  const filteredList = useMemo(() => {
    return item.filter((item) => item.includes(keyword));
  }, [item, keyword]);

  const sortItem = useMemo(() => {
    return [...filteredList].sort((a, b) =>
      sort === 'asc' ? a.localeCompare(b) : b.localeCompare(a),
    );
  }, [filteredList, sort]);
  return (
    <>
      <div>
        <input
          type="text"
          value={keyword}
          onChange={(e) => setKeyword(e.target.value)}
          placeholder="검색어를 입력하세요"
        />
        <button onClick={() => setSort('asc')}>오름차순</button>
        <button onClick={() => setSort('desc')}>내림차순</button>

        <ul>
          {sortItem.map((item) => (
            <li key={item}>{item} </li>
          ))}
        </ul>
      </div>
    </>
  );
};

위의 코드에서는 실제 검색등에서 자주활용되는, 사용자의 입력에 따라 목록을 필터링하는 예제로 keyword를 사용했습니다.

useMemo  가 활용된 것은 두개의 경우를 가정하고 사용합니다. 

첫번째로, filteredList 에서는 값이 변경될 때만 재계산 되도록 처리한 것입니다.

두번째는, filteredList 배열이나, sort의 값이 변경될 때만 재정렬되도록 처리합니다.

 

이렇게 하면, 값이 변경되지 않았을때, 이전에 계산된 값을 그대로 재활용하여 연산을 최소화합니다. 이렇게 하면, keyword 의 목록의 양이 크거나, 계산해야하는 양이 증가할때 성능 최적화의 이점을 살릴 수 있습니다.

 

2) React.memo

 

React에서 제공하는 성능 최적화 메커니즘 중 하나입니다.

하지만 이는 기존의 use 시리즈처럼, hook 이 아니라, 고차 컴포넌트(Higher Order Component) 입니다.

useMemo 와 React.memo 는 공통적으로 주의가 필요한 최적화 기법이지만, 메모제이션을 활용하는 점은 비슷합니다.

 

useMemo 의 경우, 메모제이션 기법을 활용하여, 값이 변경되지 않았다면 기존의 계산값을 재활용해 연산의 최적화를 구현하는 Hook입니다.

하지만 React.memo 는 Component 를 감싸서, Props 가 변경되었을 때만 Component 가 재랜더링 되게 합니다.

즉, useMemo 는 Component 내의 값의 계산을 최적화 하는 것이 목적이라면 React.memo 는 Component 자체의 재랜더링을 방지하는데 중점을 갖는 기능입니다.

 

사용법은 간단합니다.

const SampleComponent = React.memo(TestComponent)

이렇게 컴포넌트를 선언하여, React.memo 에 적용할 컴포넌트를 사용해주시면 됩니다.

 

** 하지만 두가지 모두 주의해야 하는 점이 있습니다. 

두가지 모두 메모제이션 기법을 사용하기 때문에, 메모리 사용량이 증가합니다. 따라서, 실제 성능에 문제가 발생하지 않는다면 이를 남발하는 것은 오히려 성능에 악영향을 미칠 수 있습니다. 

 

3) useCallback 

 

useCallback 역시 메모제이션 기법을 사용하는 성능 최적화 기능입니다.

이미 우리가 여러가지를 학습했기에, 이름만으로도 유추가 가능합니다. 주로 Callback 이라는 단어를 보면 보통 "Callback Function", 즉 함수를 떠올리게 됩니다.

 

단어 그대로 useCallback 은 함수를 재생성하는 것을 방지하고, 함수를 메모제이션 하는 기능입니다.

사용법을 위한 예제 코드를 간단하게 아래에 작성해두겠습니다.

 

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

이 역시, 무분별하게 사용하면 메모리 사용량 증가로 인해 성능문제를 일으킬 수도 있지만, 이 최적화 기능은 보다 자주 사용해야할 상황이 발생할 수 있습니다. 

바로 함수형 컴포넌트 내에서 함수를 선언하면, 컴포넌트가 재랜더링 될때마다, 함수가 재생성되는 상황이 발생합니다. 이로 인해 재랜더링이 많이 발생하면, 함수로 인해 성능 문제가 발생할 수 있습니다.

 

useCallback 을 사용하면, 의존성 배열의 값이 변경될 때만, 함수가 재생성 되기때문에, 불필요한 재생성을 방지할 수 있기 때문입니다. 

 

4. 상태 관리 로직 분리하기 

 

여기에서도 우리는 React 의 hook 중 하나인, useReducer 에 대해서 간단하게 알아보려고 합니다.

이 hook 은 useState 보다 더 복잡한 컴포넌트 상태의 동작을 관리할 때 사용합니다.

Reducer 함수와 같이 사용되는 이 hook은 이전 포스팅에서 다루었던 switch/case 문을 활용하여 작성할 수 있습니다.

예제코드 먼저 작성 후 설명으로 대체하겠습니다.

 

import React, { useReducer } from 'react';

const initialState = 0;

const counterReducer = (state, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1;
    case 'DECREMENT':
      return state - 1;
    default:
      return state;
  }
}

const Counter = () => {
  const [count, dispatch] = useReducer(counterReducer, initialState);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>Increment</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>Decrement</button>
    </div>
  );
}

export default Counter;

먼저 별도의 함수를 사용하여 counterReducer 를 만들고, 매개변수로 state, action 을 사용합니다.

switch & case 문을 사용하여 각 action.type 에 맞는 dispatch 함수를 작성합니다. 

위의 예시에서는 Increment, decrement 라는 각각의 상태 변화를 위한 dispatch 함수를, default 로 현재 상태를 반환하는 것으로 작성했습니다.

 

그리고 함수형 컴포넌트 내에 count, dispatch 를 useReducer 에, 리듀서 함수와, 초기 상태를 인자로 받아, 현재 상태와 디스패치 함수를 쌍으로 제공하는 튜플을 반환하도록 작성합니다. 

이제 실제 클릭이벤트가 발생할때, dispatch 에서 actionType 을 전달하면, 지정된 case에 맞는 함수가 실행됩니다.

 

5. React Component Tree 데이터 처리하기

 

실제 계층구조를 살펴보면 상황에 따라, 일부 Props 가 거쳐가기만 하는 Props 가 존재할 수 있습니다.

예를 들어 CRUD 에서 작성할 경우, List 에서는 remove, edit 과 같은 props 는 실제 사용되지 않고 전달만 처리됩니다.

이를 일반적으로  Props Drilling 이라고 합니다.

이를 개선하기 위해서 Context API 를 활용하여, Provder 라는 중간자 역활을 구현할 수 있습니다.

그림상으로 표현하면 아래와 같습니다.

(작성자가 그림을 잘 못그립니다.)

 

즉, 개별적으로 처리하는 것이 아니라, App Component 에는 모든 데이터를 내려서, Provider 에게 전달하고, 각각의 요소에 직접 전달할 수 있도록 처리할 수 있습니다.

여기서 핵심이 되는 Provider 가 중심이되어 처리하는 이것을 Context 라고 합니다. 즉 Provider 에서 모든 데이터를 관리한다는 "문맥" 속에서 처리되는 흐름을 뜻하기도 합니다.

 

당연히 Provider 문맥에 속하지 않은 다른 Component 가 존재할 경우, 이는 별도로 처리되므로, 당연히 Props 에 접근할 수 없게 됩니다. 이렇게 하면 위에서 말한 Props Drilling 문제를 해결할 수 있습니다.

 

사용방법 역시 간단합니다.

아래의 작성 예시를 참고하시기 바랍니다.

 

const Context = React.createContext(defaultValue);

<Context.Provider value={Props}>
// 자식 Component
</Context.Provider>

물론 전역 상태관리등을 통해 좀더 손쉽고 효율적인 처리가 가능한 방향도 존재합니다.

하지만 위 글에서는 React 자체의 기능의 설명을 주안을 두었기 때문에, 이를 감안해주시면 좋을 거 같습니다.

반응형

댓글