[React] 리액트의 개념과 성능 최적화 팁

요즘IT 아티클(알아두면 유용한 '리액트' 개념과 성능 최적화 팁) 기반 정리글 입니다.

 

리액트의 기본 개념

컴포넌트 기반 아케텍처

웹 애플리케이션의 복잡한 UI를 재사용 가능한 작은 단위로 분할하는 방식

각 컴포넌트는 상태와 속성을 갖고있음

관심사를 분리 / UI를 계층적으로 구조화하여 가독성을 높임 / 유지보수를 용이하게 함

 

유의할 부분

- 구성요소간 의존성을 최소화

- 각 컴포넌트는 한가지 책임만 진다 (단일 책임 원칙)

- 다른 개발자도 컴포넌트를 이해하고 사용하기 쉽도록 속성과 반환값을 일관되게 작성해야 함

 

JSX 문법

HTML과 유사한, 자바스크립트를 확장한 문법

컴포넌트 렌더링 로직과 마크업을 한 곳에서 관리할 수 있음

 

유의할 점

- 모든 태그는 반드시 닫혀 있어야 함

- 최상위 요소는 하나여야 함

- 카멜케이스 속성명 사용

- 중괄호를 사용해 자바스크립트 표현식 사용

- 조건부 렌더링은 if 또는 삼항연산자 사용

- 인라인 스타일은 style={{}} 사용

- 주석 작성은 {/* ~~~~*/} 사용

가상 DOM

실제 DOM을 추상화한 DOM

컴포넌트가 처음 렌더링될 때 가상 돔 트리를 생성하고, 이후 상태나 속성이 변경되면 비교와 조정 (Diffing and Reconciliation)을 거쳐 변경된 부분만 실제 DOM에 반영(patch) -> 불필요한 DOM 조작을 최소화

 

props와 state

리액트 컴포넌트에서 데이터를 관리하는 두가지 주요 개념

 

 

리액트 훅(React Hooks)의 활용

리액트 훅

컴포넌트 내에서 리액트 기능을 사용할 수 있게 해주는 일종의 함수 API

컴포넌트 간 상태를 공유하거나 불필요한 렌더링을 방지하여 성능을 최적화하는 등의 역할을 수행할 수 있음

ex. useState, useRef, useEffect, useMemo, useReducer

 

 

useState

컴포넌트에서 상태를 관리하기 위한 훅

상태 값과 상태를 업데이트하는 함수를 받을 수 있음

 

useRef

컴포넌트 내에서 특정 값을 저장하고 참조할 수 있게 해줌

useRef로 생성한 ref 객체는 컴포넌트 생명주기 동안 유지되며, 값이 변경되어도 컴포넌트가 다시 렌더링되지 않음!

주로 DOM 엘리먼트에 직접 접근해야 하거나, 이전 값을 저장해야 할 때 사용

current 속성으로 값에 접근할 수 있다

 

import { useRef } from 'react';

const TextInputWithFocusButton = () => {
  const inputEl = useRef(null);  // 초기값이 null인 객체 생성
  
  const onButtonClick = () => {
    inputEl.current.focus();
  };
  
  return (
    <>
      <input ref={inputEl} type="text"/>
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

export default TextInputWithFocusButton;

 

useEffect

컴포넌트의 side effect를 처리하기 위해 사용

컴포넌트가 렌더링된 후 실행되며, 두 인자를 받음

1) side effect 함수 -> 의존성배열에 변화가 생길 때 실행할 동작

2) 의존성 배열

- 만약 의존성 배열을 빈 배열로 넣으면, 컴포넌트가 마운트될 때만 side effect 함수가 실행

- 의존성 배열을 생략하면 컴포넌트가 업데이트 될 때마다 실행

 

useMemo

계산량이 많은 함수의 반환값을 모아 memoization하여 불필요한 중복 계산을 방지

두개의 인자를 받음

1) memoization할 함수

2) 의존성 배열

- 의존성배열의 값이 변경되지 않는 이상 이전에 계산된 값을 재사용함

- 의존성 배열에 빈 배열을 넣으면 컴포넌트가 마운트될 때만 함수가 실행

- 의존성 배열을 생략하면 컴포넌트가 업데이트될 때마다 함수가 실행

 

 

useReducer

useState와 같이 컴포넌트의 상태를 관리하기 위한 훅

useState는 컴포넌트 내에 상태를 업데이트하는 로직을 두어야 하는 반면, useReducer는 상태 업데이트 로직을 컴포넌트 외부에 둘 수 있음

중복되는 상태 업데이트 로직을 한 고셍 모아 관리할 수 있음

특히 여러개의 상태를 관리해야하거나, 프로젝트 규모가 큰 경우 유용하게 사용할 수 있음

 

import {useReducer} from 'react';

const initalState = { count: 0 };

// 상태와 액션 객체를 인자로 받음
const reducer = (state, action) => {
  switch(action.type) {
    case 'increment':
      return { count: state.count + 1 };  // 새로운 상태를 반환
    case 'decrement':
      return { count: state.count - 1 };
    default:
      throw new Error('Unsupported action type');
  }
}

const Counter = () => {
  const [state, dispatch] = useReducer(reducer, initialState);
  
  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => {dispatch({type: 'increment'})}>+</button>
      <button onClick={() => {dispatch({type: 'decrement'})}>-</button>
    </div>
  );
}

export default Counter;

 

커스텀 훅 만들기

커스텀 훅은 개발자가 직접 만들어 사용하는 훅을 의미함

커스텀 훅을 사용하면 컴포넌트 간 중복되는 로직을 제거하여 코드의 가독성을 높일 수 있음

커스텀 훅 작성시 use로 시작하는 함수명을 써야하며, 커스텀 훅 내부에 다른 리액트 훅을 사용할 수 있음

 

// 입력 필드를 관리하는 useInput 커스텀 훅 예시
import {useState} from 'react';

const useInput = (initialValue = '') => {
  const [value, setValue] = useState(initialValue);
  
  const handleChange = (event) => {
    setValue(event.target.value);
  };
  
  return [value, handleChange];
}

export default useInput;

// useInput을 사용할 jsx
const InputComponent = () => {
  const [username, setUsername] = useInput('');
  const [password, setPassword] = useInput('');
  
  const handleSubmit = (event) => {
    event.preventDefault();
    console.log('Username: ', username);
    console.log('Password: ', password);
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input type="text" value={username} onChange={setUsername} placeholder="Username"/>
      <input type="password" value={password} onChange={setPassword} placeholder="Password"/>
      <button type="submit">Submit</button>
    </form>
  );
}

 

훅 사용시 주의사항

- 리액트 훅을 사용할 때는 컴포넌트나 커스텀 훅의 최상위 레벨에서만 호출해야 함

- 즉, 일반 JS 함수 / 반복문 / 조건문 내에서는 훅을 호출할 수 없음 (호출 순서가 일관되지 않은 경우들)

- 리액트가 컴포넌트 랜더링 시, 훅이 동일한 순서로 호출될 것이라고 가정하기 때문

 

- 훅을 과도하게 사용하면 복잡성을 증가시킬 수 있음

- 상태와 로직을 적절히 추상화하여 불필요한 코드를 추가하지 않도록 해야 함

 

- 의존성 배열을 정확히 명시하여 불필요한 렌더링을 방지해야 함

 

 

리액트 컴포넌트의 종류

클래스형 컴포넌트 vs 함수형 컴포넌트

1) 클래스형 컴포넌트

- 클래스 문법을 사용, 상태와 생명주기 메서드를 가짐

- 생명주기와 관련된 복잡한 로직을 구현할 때 장점이 있지만, 코드가 길어져 가독성이 떨어지고 재사용성이 낮아질 수 있음

 

2) 함수형 컴포넌트

- 간단한 함수로 정의됨

- 기존의 자바스크립트 함수 표현식으로 쓸 수 있고, 화살표함수를 사용해 정의할 수도 있음

- 클래스형 컴포넌트에 비해 코드가 간결하고 테스트와 디버깅이 용이해 함수형 컴포넌트를 권장하고 있음

 

컴포넌트 간 데이터 전달 방법

- 기본적으로 props를 사용

- 실무에서 자식에서 부모로 데이터를 전달해야 하는 경우 부모 컴포넌트에서 콜백함수를 props로 전달하고, 자식 컴포넌트에서 해당 함수를 호출하는 방식으로 처리하기도 함

- useCallback 훅을 사용해 부모 컴포넌트가 렌더링될 때 불필요하게 자식 컴포넌트가 렌더링되지 않도록 하는 것이 중요!

 

import { useCallback } from 'react';

const ParentComponent = () => {
  const [data, setData] = useState('');
  
  // 부모 컴포넌트에서 자식 컴포넌트에서 전달된 데이터를 처리할 콜백함수 정의, 이 때 useCallback 사용 !
  const handleCallback = useCallback((childData) => {
    setData(childData);
  });
  
  return (
    <div>
      <h1>Parent Component</h1>
      <p>Data from child: {data}</p>
      <ChildComponent onCallback={handleCallback}/>
    </div>
  );
}

const ChildComponent = ({onCallback}) => {
  const handleClick = () => {
    onCallback('Data from child');  // 자식 컴포넌트의 데이터를 부모 컴포넌트에 전달
  };
  
  return (
    <div>
      <h2>Child Component</h2>
      <button onClick={handleClick}>Send data to parent</button>
    </div>
  );
}

 

프로젝트 규모가 커지는 경우 리액트에서 제공하는 Context API 혹은 Redux, MobX, Zustand 같은 상태관리 라이브러리를 사용해 전역 상태를 관리하기도 함

 

 

리액트 개발자 도구와 프레임워크

리액트 개발자 도구

- 크롬 웹스토어와 파이어폭스 애드온에서 다운받아 사용할 수 있는 브라우저 확장 도구

- 리액트 컴포넌트 계층 구조를 시각적으로 보여줘 컴포넌트 간의 관계를 쉽게 파악할 수 있으며 각 컴포넌트의 props, state를 실시간으로 확인할 수 있어 데이터의 흐름을 추적하고 디버깅하는 데에 필수적인 도구

 

리액트 프레임워크

- 리액트 자체만으로는 코드 분할, 라우팅, 데이터 패칭 등을 구현하기 어렵기 때문에 추가적인 도구나 라이브러리를 사용해야 함

- Next.js, Remix, Gatsby, Astro 등

 

 

리액트 성능 최적화 팁

불필요한 렌더링 방지

컴포넌트 렌더링은 애플리케이션 성능에 큰 영향을 미치므로, 성능 최적화를 위해선 불필요한 렌더링을 방지하는 것이 중요

함수형 컴포넌트에서 특정 props의 변화에만 컴포넌트가 렌더링되게 하려면 React.memo를 사용할 수 있음

 

import { useState } from 'react';

// name이 변경될 때만 자식 컴포넌트를 리렌더링
const ChildComponent = React.memo(({ name }) => {
  console.log('Child component rendered');
  return <p>Hello, {name}!</p>;
});

const ParentComponent = () => {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('John');
  
  const incrementCount = () => {
    setCount(prevCount => prevCount + 1);
  };
  
  const changeName = () => {
    setName('Jane');
  };
  
  return (
    <div>
      <button onClick={incrementCount}>Increment Count</button>
      <button onClick={changeName}>Change Name</button>
      <p>Count: {count}</p>
      <ChildComponent name={name}/>
    </div>
  );
};

export default ParentComponent;

 

고차 컴포넌트 (High-Order Component)

: 컴포넌트를 인자로 받아 새 컴포넌트를 반환하는 함수

 

- React.memo는 고차 컴포넌트

- 함수형 컴포넌트를 인자로 받아 메모이제이션 된 컴포넌트를 반환

- 따라서 부모 컴포넌트에서 자식 컴포넌트와 관련이 없는 count 상태 값이 변경되더라도 자식 컴포넌트에 대한 불필요한 렌더링이 발생하지 않음

 

- React.memo를 사용해 특정 props의 변경에만 렌더링되도록 조건을 설정할 수 있음

- 하지만 React.memo는 얕은 비교를 하기 때문에 props가 함수이거나 객체인 경우 자식 컴포넌트의 렌더링이 발생할 수 있음

- 이를 해결하기 위해 아래 코드처럼 useCallback, useMemo와 같은 훅을 사용하기도 함

- 하지만 useCallback, useMemo와 같은 훅은 추가 메모리가 필요하므로 무분별하게 사용하는 경우 오히려 성능이 저하될 수 있음

 

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

const ChildComponent = React.memo(({name, onIncrement}) => {
  return (
    <div>
      <p>Hello, {name}!</p>
      <button onClick={onIncrement}>Increment Count</button>
    </div>
  );
});

const ParentComponent = () => {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('John');
  
  const incrementCount = useCallback(() => {
    setCount(prevCount => prevCount + 1);
  }, []);
  
  const childProps = useMemo(() => {
    return { name, onIncrement: incrementCount };
  }, [name, incrementCount]);
  
  return (
    <div>
      <p>Count: {count}</p>
      <ChildComponent {...childProps}/>
    </div>
  );
}

 

 

코드 스플리팅과 레이지 로딩

코드 스플리팅 (code splitting)

: 번들링된 자바스크립트 코드를 여러 개의 작은 조각 단위로 분할하는 것

 

- 일반적으로 리액트 애플리케이션은 모든 코드를 하나의 큰 번들로 빌드하여 배포

- 이는 초기 로딩 시간을 길어지게 할 수 있음

- 반면 코드 스플리팅을 하는 경우 필요한 코드만 동적으로 로드하여 초기 번들 크기를 줄이고 로딩 속도를 개선할 수 있음

 

- 리액트에서 코드 스플리팅을 구현하기 위해서는 React.lazy() 함수와 Suspense 컴포넌트를 이용한 레이지 로딩 (Lazy Loading)을 이용하기도 함

 

import { Suspense } from 'react';

// React.lazy를 사용해 컴포넌트 로딩을 지연
const LazyComponent = React.lazy(() => import('./LazyComponent'));

const MyComponent = () => {
  return (
    <div>
      // Suspense 컴포넌트로 레이지로딩된 컴포넌트가 로드되는 동안 fallback UI를 보여줌
      <Suspense fallback={<div>Loading...</div>}>
        <LazyComponent/>
      </Suspense>
    </div>
  )
}

'React' 카테고리의 다른 글

[React] useState  (0) 2024.08.16
[React] styled-component  (0) 2024.08.16
[React] Rendering과 Virtual DOM  (0) 2024.08.08
[React] state  (0) 2024.08.08
[React] props와 children  (0) 2024.08.08