실제 운영 중인 웹 서비스의 특정 페이지에서 이상한 점을 발견했다.

어떤 페이지는 빠르게 로딩되고, 어떤 페이지는 눈에 띄게 느렸다.

사용자들도 "왜 이 페이지만 느려요?"라는 문의를 자주 했다. 무엇이 문제인지 찾아보기로 했다.


문제 발견

Chrome DevTools의 Network 탭으로 각 페이지의 API 호출 패턴을 확인했다.

예시

  1. F12로 개발자 도구 열기
  2. Network 탭에서 페이지 새로고침 후 XHR/Fetch 필터 적용
  3. 사용자 정보 관련 API 호출 패턴 확인
  4. 각 페이지별 호출 횟수와 데이터 크기 측정

결과는 다음과 같았다.

/관리자 페이지 A 0회 0 kB
/관리자 페이지 B 1회 1.1 kB
/관리자 페이지 C 1회 1.1 kB
/관리자 페이지 D 2회 2.2 kB

같은 사용자 정보 관련 API를 페이지마다 다른 횟수로 호출하고 있었다.


성능 측정

실제 사용자가 체감하는 성능 차이를 확인하기 위해 Lighthouse로 측정해봤다.

프로덕션 환경에서 측정한 결과는 다음과 같았다.

 

관리자 페이지 D  페이지 (API 2회 호출)

  • Performance Score: 20/100점 (Poor)

관리자 페이지 A  페이지 (API 0회 호출)

  • Performance Score: 69/100점 (Needs Improvement)

차이가 49점이었다. 245%의 성능 격차가 났다.


원인 분석

코드를 확인해보니 각 페이지마다 사용자 정보를 가져오는 방식이 달랐다.

최적화된 페이지는 이미 로딩된 데이터를 재사용했다.

// 예시 : 최적화된 페이지
const TeachersPage = () => {
  const [userInfo] = useAtom(userAtom);
  
  if (!checkUserData(userInfo)) {
    return <LoadingSpinner />;
  }
  
  return <TeachersTable />;
};

 

문제가 있는 페이지는 컴포넌트에서 독립적으로 API를 호출했다.

// 예시 : 문제가 있는 페이지
const DocumentHistoryPage = () => {
  const [userInfo] = useAtom(userAtom);
  
  useEffect(() => {
    fetchUserProfile(); // 첫 번째 호출
  }, []);
  
  useEffect(() => {
    if (someCondition) {
      fetchUserProfile(); // 두 번째 호출
    }
  }, [dependency]);
  
  return <HistoryTable />;
};

해결 방법

Layout 레벨에서 한 번만 API를 호출하고, 모든 페이지에서 캐시된 데이터를 사용하도록 변경했다.

// 예시 : _layout.jsx
const Layout = ({ children }) => {
  const [userInfo, setUserInfo] = useAtom(userAtom);
  
  useEffect(() => {
    if (!userInfo) {
      fetchUserProfile().then(setUserInfo);
    }
  }, []);
  
  return (
    <div>
      <Header />
      {children}
      <Footer />
    </div>
  );
};

모든 페이지에서 일관된 패턴을 사용하도록 수정했다.

const OptimizedPage = () => {
  const [userInfo] = useAtom(userAtom);
  
  if (!checkUserData(userInfo)) {
    return <LoadingSpinner />;
  }
  
  return <PageContent />;
};

기존에 각 페이지에서 개별적으로 호출하던 fetchUserProfile() 함수 호출을 제거했다.


개선 결과

Lighthouse 점수 20점 71점 3.55배 향상
API 호출 횟수 2회 0회 개선
데이터 전송량 2.2 kB 0 kB 개선
성능 등급 Poor Needs Improvement 2단계 상승

Network 탭으로 확인한 결과, 관리자 페이지 D에서 사용자 정보 API 호출이 완전히 제거되었다.

 


배운 점

  • 추측이나, 감이 아닌 Chrome DevTools와 Lighthouse 같은 도구로 성능에 대한 객관적인 데이터를 얻는것이 중요하다.
  • 같은 팀에서 개발해도 페이지마다 다른 패턴이 적용될 수 있다는 점을 확인했다. 정기적인 코드 리뷰와 아키텍처 가이드라인이 필요하다.
  • 단순해 보이는 API 중복 호출 제거로 성능 향상을 시킬 수 있다.

 

React 프로젝트에서 특정 컴포넌트를 살펴보면 component.jsx 파일과 index.js 파일이 함께 존재하는 경우가 많습니다. 이 두 파일의 역할과 함께 사용하는 이유를 정리해 보겠습니다.


1. component.jsx의 역할

component.jsx 파일은 해당 컴포넌트의 실제 구현을 담당하는 파일입니다. 보통 React의 function component 또는 class component를 정의하고, 필요한 JSX 및 로직을 포함합니다.

import React from 'react';

const MyComponent = () => {
  return <div>안녕하세요, 저는 MyComponent입니다!</div>;
};

export default MyComponent;

 

이처럼 component.jsx 파일에는 해당 컴포넌트의 UI와 기능이 정의됩니다.


2. index.js의 역할

같은 디렉터리에 위치한 index.js 파일은 보통 해당 컴포넌트를 exportexport 하는 역할을 합니다.

export { default } from './component';

 

이렇게 하면, 다른 곳에서 이 컴포넌트를 가져올 때 폴더 경로만 지정하면 됩니다.

 

 

예제: index.js 사용 전후 비교

(1) index.js 없이 직접 import 하는 경우

import MyComponent from '../components/MyComponent/component';

 

(2) index.js를 활용하는 경우

import MyComponent from '../components/MyComponent';

 

이처럼 index.js 파일을 두면 import 경로를 짧고 간결하게 유지할 수 있습니다.


3. index.js를 함께 사용하는 이유

폴더 경로를 간결하게 유지

  • index.js를 사용하면 컴포넌트를 가져올 때 폴더 경로를 직접 지정할 수 있어 코드가 깔끔해집니다.

파일 구조의 일관성 유지

  • 모든 컴포넌트 폴더에 index.js를 포함하면 프로젝트 구조가 일관성을 갖게 됩니다.

추후 확장 가능성 증가

  • 여러 개의 관련된 컴포넌트를 한 폴더에서 관리할 때, index.js를 이용해 한꺼번에 export 할 수도 있습니다.
export { default as Header } from './Header';
export { default as Footer } from './Footer';
export { default as Sidebar } from './Sidebar';

 

이렇게 하면 한 번의 import로 여러 개의 컴포넌트를 가져올 수 있습니다.

import { Header, Footer, Sidebar } from '../components/Layout';

Summary

React에서 component.jsx와 index.js가 함께 존재하는 이유는 코드의 가독성과 유지보수성을 높이고,  import 경로를 단순화하기 위해서입니다. 

index.js를 사용하면 폴더 구조를 정리할 때 더 체계적으로 관리할 수 있고, 프로젝트의 일관성을 유지하는 데에도 큰 도움이 됩니다.

 

오늘은 react-app-rewired 설정을 통해 React 프로젝트의 빌드 파일 구조를 커스터마이징하고, 필요에 따라 react-app-rewired를 삭제한 후 빌드 파일 정리까지 하는 과정을 알아보고자 합니다.


1. react-app-rewired 설정하기

React 프로젝트에서는 기본적으로 create-react-app을 사용하면 빌드 파일의 구조가 고정되어 있습니다. 이때, react-app-rewired를 사용하면 Webpack 설정을 수정할 수 있습니다.

 

1.1 react-app-rewired 설치

먼저 프로젝트 루트 경로에서 다음 명령어를 실행하여 react-app-rewired와 customize-cra를 설치합니다.

npm install react-app-rewired customize-cra --save-dev

 

 

1.2 config-overrides.js 파일 생성

프로젝트 루트 디렉터리에 config-overrides.js 파일을 생성하고, 다음과 같이 Webpack 설정을 수정합니다.

const path = require('path');

module.exports = function override(config, env) {
  if (env === 'production') {
    // 빌드 output 경로 및 파일 이름 설정
    config.output = {
      ...config.output,
      path: path.resolve(__dirname, 'build'),
      filename: 'js/[name].[contenthash:8].js',
      publicPath: '/',
    };

    // CSS 파일의 경로 및 이름 설정
    const miniCssPlugin = config.plugins.find(
      (plugin) => plugin.constructor.name === 'MiniCssExtractPlugin'
    );

    if (miniCssPlugin) {
      miniCssPlugin.options.filename = 'css/[name].[contenthash:8].css';
    }
  }
  return config;
};

 

1.3 package.json 수정

React의 빌드, 시작 스크립트를 react-app-rewired로 변경합니다.

"scripts": {
  "start": "react-app-rewired start",
  "build": "react-app-rewired build",
  "test": "react-app-rewired test"
}

2. 빌드 파일 정리: static 폴더 삭제하기

빌드 후 static 폴더 내부의 불필요한 파일을 삭제하고 싶다면 rimraf를 사용하여 자동으로 삭제할 수 있습니다.

 

2.1 rimraf 설치

rimraf는 Node.js 환경에서 파일과 폴더를 삭제할 수 있는 유틸리티입니다. 다음 명령어로 설치합니다.

npm install rimraf --save-dev

 

React에서 List와 Key는 배열 데이터를 화면에 렌더링 할 때 중요한 역할을 합니다.

특히, 여러 개의 컴포넌트를 반복적으로 렌더링해야 할 때 필수적으로 사용됩니다. 


1. React에서 배열 렌더링하기

React는 배열 데이터를 기반으로 여러 개의 엘리먼트를 효율적으로 렌더링 할 수 있습니다.

배열의 각 요소를 화면에 출력하려면 map() 메서드를 주로 사용합니다.

function NameList() {
 const names = {"Alice", "Bob", "Charlie"};
 
 return (
  <ul>
   {names.map((name, index) => (
    <li key={index}>{name}</li>
   ))}
  </ul>
 );
}

names 배열의 각 요소를 <li>로 변환하여 리스트 형태로 화면에 출력합니다.


2. Key란?

React에서 Key는 배열이나 리스트를 렌더링할 때, 각 요소를 식별하기 위한 고유한 값입니다. Key는 React가 어떤 항목이 변경, 추가 또는 삭제되었는지 효율적으로 추적하도록 도와줍니다. 이를 통해 성능을 최적화하고 불필요한 렌더링을 방지할 수 있습니다.

 

Key의 특징

  • Key는 리스트 내에서만 고유하다.
  • Key는 변하지 않는 값이어야 한다.

3. Key를 사용하는 이유

React는 Key를 사용하여 각 리스트 항목을 구별합니다. Key가 없다면 React는 리스트의 변화를 추적하기 어려워지고, 불필요한 렌더링이 발생할 수 있습니다.

function NameList() {
  const names = ["Alice", "Bob", "Charlie"];

  return (
    <ul>
      {names.map((name) => (
        <li>{name}</li> // Key가 없음
      ))}
    </ul>
  );
}

Key가 없으면 React는 각 항목의 고유성을 알 수 없으므로 경고 메시지를 표시합니다.


4. Key의 올바른 사용법

Key로 값을 사용하는 경우

배열의 값 자체가 고유하다면, 해당 값을 Key로 사용할 수 있습니다.

function NameList() {
  const names = ["Alice", "Bob", "Charlie"];

  return (
    <ul>
      {names.map((name) => (
        <li key={name}>{name}</li> // name 값을 Key로 사용
      ))}
    </ul>
  );
}

 

Key로 아이디를 사용하는 경우

데이터에 고유한 ID가 포함되어 있다면, 이를 Key로 사용하는 것이 가장 권장됩니다.

function UserList() {
  const users = [
    { id: 1, name: "Alice" },
    { id: 2, name: "Bob" },
    { id: 3, name: "Charlie" },
  ];

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li> // id 값을 Key로 사용
      ))}
    </ul>
  );
}

 

Key로 인덱스를 사용하는 경우

Key로 인덱스를 사용하는 것은 데이터가 자주 변경되지 않을 경우에만 적합합니다. 예를 들어, 리스트의 항목이 추가되거나 순서가 변경되면 Key 값이 달라져 React의 최적화가 제대로 작동하지 않을 수 있습니다.

function NameList() {
  const names = ["Alice", "Bob", "Charlie"];

  return (
    <ul>
      {names.map((name, index) => (
        <li key={index}>{name}</li> // index를 Key로 사용
      ))}
    </ul>
  );
}

5. Key가 반드시 필요한 경우

React에서는 map() 메서드로 생성된 리스트의 각 요소에 Key를 지정해야 합니다. Key는 React가 DOM 변화를 효율적으로 처리하기 위필요합니다.

function NameList() {
  const names = ["Alice", "Bob", "Charlie"];

  return (
    <ul>
      {names.map((name) => (
        <li>{name}</li> // Key가 없어 경고 발생
      ))}
    </ul>
  );
}

 


Summary

React에서 리스트를 렌더링할 때 Key는 필수적이며, 효율적인 렌더링의 핵심 요소입니다. 

  • Key는 리스트 내에서 고유해야 한다.
  • Key로 값, 아이디, 인덱스를 사용할 수 있다.
  • Key가 없으면 경고가 발생하며 React의 최적화가 제대로 작동하지 않을 수 있다.

조건부 렌더링(Conditional Rendering)은 React에서 UI를 동적으로 변경하는 매우 중요한 개념입니다.

조건에 따라 화면에 표시되는 요소를 제어할 수 있기 때문에, 사용자가 입력한 값, 상태 변화, 또는 외부 데이터에 따라 화면을 변화시킬 수 있습니다.


1. 조건부 렌더링이란?

특정 조건에 따라 화면에 표시되는 요소가 달라지는 방식입니다. 예를 들어, 사용자가 로그인했을 때와 로그인하지 않았을 때 보여주는 UI가 달라야 할 경우, 조건부 렌더링을 사용하여 이러한 동작을 구현할 수 있습니다.

//ture일 때 버튼을 보이고, false일 때 버튼 숨기기

function App() {
 const isLoggedIn = true;
 
 return (
  <div>
   {isLoggedIn ? <button>로그아웃</button> : null}
  </div>
 );
}

isLoggedIn이 true 일 때만 "로그아웃" 버튼이 표시되고, false일 경우 버튼이 보이지 않습니다.


2. JavaScript의 Truthy와 Falsy 값

React에서 조건부 렌더링을 할 때는 JavaScript의 Truthy와 Falsy 값 개념을 잘 이해하는 것이 중요합니다.

JavaScript는 다양한 값들을 조건문에서 true 또는 false로 평가합니다. 

이를 Truthy(참)와 Falsy(거짓) 값으로 나누며, 조건문에서 이들 값을 쉽게 사용할 수 있습니다.

  • Falsy 값 : false, 0, ""(빈 문자열), null, undefined, NaN
  • Truthy값 : 위에 나열된 값을 제외한 모든 값
function App() {
 const isLoggedIn = 0;
 
 return (
  <div>
   {isLoggedIn && <button>로그아웃</button>} //isLoggedIn이 falsy이므로 버튼이 렌더링되지 않음
  </div>
 );
}

isLoggedIn이 0인 경우, false로 평가되어 버튼은 렌더링 되지 않습니다.


3. Inline Conditions (조건문을 코드 안에 집어넣기)

React에서는 조건문을 JSX 코드 안에 간단하게 넣을 수 있습니다. 이를 Inline Conditions라고 하며, 여러 방법으로 조건부 렌더링을 구현할 수 있습니다.

 

Inline if(&& 연산자 사용)

가장 간단한 조건부 렌더링 방식은 && 연산자를 사용하는 것입니다. 이 방식은 조건이 true일 때만 특정 요소를 렌더링 하고, false일 경우 아무것도 렌더링 하지 않습니다.

function App() {
 const isLoggedIn = true;
 
 return (
  <div>
   {isLoggedIn && <button>로그아웃</button>} //isLoggedIn이 true일 때만 버튼이 보임
  </div>
 );
}

 

Inline If-Else (삼항 연산자 사용)

삼항 연산자는 조건문을 JSX 내부에서 더 간결하게 작성할 수 있는 방법으로 기본 문법은 condition ? true : false 형태로 사용됩니다.

function App() {
  const isLoggedIn = false;

  return (
    <div>
      {isLoggedIn ? <button>로그아웃</button> : <button>로그인</button>}  {/* isLoggedIn에 따라 버튼이 달라짐 */}
    </div>
  );
}

isLoggedIn이 true일 경우 "로그아웃"버튼을, false일 경우 "로그인" 버튼을 렌더링 합니다.


4. Component 렌더링 막기 : null 리턴하기

특정 조건에서 컴포넌트를 렌더링 하지 않도록 할 때는, JSX 내에서 null을 리턴하는 방법을 사용할 수 있습니다.

null을 리턴하면 해당 컴포넌트는 아예 렌더링이 되지 않습니다.

function App() {
  const isLoggedIn = false;

  return (
    <div>
      {isLoggedIn ? <button>로그아웃</button> : null}  {/* isLoggedIn이 false일 경우 버튼을 렌더링하지 않음 */}
    </div>
  );
}

isLoggedIn이 false일 경우 <button>로그아웃</button>이 전혀 렌더링 되지 않습니다.


5. 조건부 렌더링을 활용한 예시

조건부 렌더링은 로그인 상태에 따른 UI 변경이나, 데이터 로딩 상태를 처리할 때 자주 사용됩니다. 예를 들어 사용자가 로그인했을 때와 로그아웃했을 때 다르게 화면을 보여줄 수 있습니다.

function App() {
 const user = { name : "John", loggedIn : true };
 
 return (
  <div>
   {user.loggedIn ? (
    <h1>Welcome, {user.name}!</h1>
   ) : (
    <h1>로그인해주세요.</h1>
   )}
  </div>
 );
}

참고 강의 : 인프런-처음 만난 리액트

 

React에서 이벤트는 사용자 인터페이스에서 발생하는 특정 동작이나 활동을 나타냅니다. 

예를 들어 버튼 클릭, 입력 필드에 텍스트 입력, 마우스 이동 등이 이벤트에 해당됩니다.

React는 이러한 이벤트를 처리하기 위해 브라우저의 DOM 이벤트를 추상화한 Synthetic Event 시스템을 제공합니다.

이를 통해 브라우저 간의 호환성을 유지하면서 일관된 방식으로 이벤트를 다룰 수 있습니다.


카멜 표기법 사용

React에서 이벤트는 카멜 표기법(camelCase)을 사용합니다. 이는 HTML의 소문자 기반 이벤트 속성과 차이가 있습니다.

예 :

  • HTML: <button onclick="handleClick()">Click me</button>
  • React: <button onClick={handleClick}>Click me</button>

이러한 표기법은 React가 자바스크립트 표준을 따르는 방식을 반영합니다.


이벤트 핸들러(Event Handler)

이벤트 핸들러는 특정 사건이 발생했을 때 호출되는 함수입니다. React에서 이벤트 핸들러는 JSX 내에서 선언되며, 함수로 작성되어야 합니다.

function handleClick {
 alert('Button clicked!');
}

<button conClick={handleClick}>Click me</button>

위 코드에서 onClick은 이벤트 리스터(Event Listner) 역할을 하며, handleClick 함수가 이벤트 핸들러로 동작합니다.


Event Listener와 바인딩

클래스 컴포넌트에서 이벤트 핸들러를 사용할 때 this를 적절히 바인딩해야 합니다. 이를 위해 bind 메서드를 사용할 수 있습니다.

class MyComponent extends React.Component {
 constructor(props) {
  super(props);
  this.handleClick = this.handleClick.bind(this);
 }
 
 handleClick() {
  console.log('Button clicked!');
 }
 
 render() {
  return <button onClick={this.handleClick}>Click me</button>
 }
}

바인딩 없이 이벤트 핸들러 선언하기

1. Class Field Syntax 사용

Class Field Syntax를 사용하면 화살표 함수로 메서드를 정의하여 this 바인딩을 자동으로 처리할 수 있습니다.

class MyComponent extends React.Component {
  handleClick = () => { 
    console.log('Button clicked!');
  };

  render() {
    return <button onClick={this.handleClick}>Click me</button>;
  }
}

 

2. Arrow Function 사용

JSX 내부에서 화살표 함수를 사용하면 바인딩 없이도 this를 올바르게 유지할 수 있습니다.

class MyComponent extends React.Component {
  handleClick() {
    console.log('Button clicked!');
  }

  render() {
    return <button onClick={() => this.handleClick()}>Click me</button>;
  }
}

그러나 JSX에서 화살표 함수를 남용하면 성능 문제가 발생할 수 있으니 주의해야 합니다.


Arguments 전달하기

이벤트 핸들러에 데이터를 전달하려면 매개변수(Arguments)를 사용할 수 있습니다.

  • 화살표 함수 사용 : <button onClick={(e) => this.handleClick(e, 'Hello')}>Click me</button>
  • bind 메서드 사용 : <button onClick={this.handleClick.bind(this, 'Hello')}>Click me</button>

참고 강의 : 인프런-처음 만난 리액트

'REACT' 카테고리의 다른 글

[REACT] List와 Key  (0) 2025.01.02
[REACT] 조건부 렌더링(Conditional Rendering)  (1) 2024.12.31
[React] Hook의 규칙 및 사용법  (1) 2024.12.24
[React] Hook : useMemo, useCallback, useRef  (0) 2024.12.24
[React] Hook : useState와 useEffect  (0) 2024.12.24

1. Hook의 규칙

React Hook을 올바르게 사용하려면 두 가지 규칙을 꼭 지켜야 합니다.

 

(1) Hook은 최상위 레벨에서만 호출해야 합니다.

  • Hook은 컴포넌트의 최상위 레벨에서만 호출해야 합니다.
  • 조건문, 반복문, 중첩 함수 안에서 Hook을 호출하면 안 됩니다.
  • React는 Hook이 호출되는 순서에 따라 상태를 관리하기 때문에, Hook 호출 순서가 달라지면 의도하지 않은 동작이 발생할 수 있습니다.

잘못된 예시(if문에서 호출)

function MyComponent() {
  if (someCondition) {
    useEffect(() => {  // 조건문 안에서 Hook 호출
      console.log('Effect called');
    });
  }
  return <div>Hello</div>;
}

 

올바른 예시

function MyComponent() {
  useEffect(() => {
    if (someCondition) {
      console.log('Effect called');
    }
  });
  return <div>Hello</div>;
}

 


(2) React 함수 컴포넌트에서만 Hook을 호출해야 합니다.

  • Hook은 React 함수 컴포넌트 또는 Custom Hook에서만 호출할 수 있습니다.
  • 클래스 컴포넌트나 일반 자바스크립트 함수에서는 Hook을 사용할 수 없습니다.

 잘못된 예시(일반 함수에서 사용)

 

function regularFunction() {
  const [count, setCount] = useState(0); // 일반 함수에서 Hook 호출
}

 

올바른 예시(리액트 함수 컴포넌트에서 사용)

function MyComponent() {
  const [count, setCount] = useState(0); // React 함수 컴포넌트에서 사용
  return <div>{count}</div>;
}

2. 규칙을 검사하는 도구 : eslint-plugin-react-hooks

eslint-plugin-react-hooks 패키지를 사용하여 React Hook의 규칙을 준수하는지 자동으로 검사할 수 있습니다.

  • useEffect 나 useCallback에서 의존성 배열을 올바르게 설정했는지 확인 가능
  • Hook의 호출 위치가 규칙에 맞는지 검사

설치 및 설정

npm install eslint-plugin-react-hooks --save-dev

 

eslint 설정 파일에 추가

{
  "plugins": ["react-hooks"],
  "rules": {
    "react-hooks/rules-of-hooks": "error", // Hook의 규칙 검사
    "react-hooks/exhaustive-deps": "warn" // 의존성 배열 검사
  }
}

 


3. Custom Hook 만들기

custom Hook은 Hook의 로직을 재사용할 수 있도록 만들어진 사용자 정의 함수입니다.

use로 시작하는 이름을 사용해야 하며, 일반 함수와 비슷하지만 다음과 같은 특징이 있습니다.

 

Custom Hook의 특징

1. 단순한 함수 : 기존의 Hook을 조합하여 새로운 기능을 제공

2. 독립성 : Custom Hook 내부의 상태와 효과는 사용하는 컴포넌트와 독립적입니다.

 

Custom Hook이 필요한 상황

  • 여러 컴포넌트에서 중복된 상태 관리 또는 효과 로직이 반복될 때
  • 공통된 비즈니스 로직을 분리하여 유지보수를 쉽게 하고자 할 때

Custom Hook 만들기

useWindowWidth라는 Custom Hook을 만들어, 현재 브라우저 창의 너비를 관리하는 예시입니다.

import { useState, useEffect } from 'react';

function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);

  useEffect(() => {
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener('resize', handleResize);

    return () => window.removeEventListener('resize', handleResize); // Cleanup
  }, []);

  return width;
}
function MyComponent() {
  const width = useWindowWidth();

  return <div>Window width: {width}px</div>;
}

Summary

Hook은 React 애플리케이션의 상태 관리와 효과 로직을 간결하고 재사용 가능하게 만들어줍니다. 

하지만 규칙을 준수하지 않으면 의도치 않은 버그가 발생할 수 있으므로 다음과 같은 규칙을 준수해야 합니다.

  1. Hook은 컴포넌트의 최상위 레벨에서만 호출한다.
  2. React 함수 컴포넌트 또는 Custom Hook에서만 Hook을 호출한다.
  3. eslint-plugin-react-hooks를 사용해 규칙을 검사한다.

 

React에서 성능 최적화와 DOM 제어를 위해 자주 사용되는 고급 Hook으로 useMemo, useCallback, useRef가 있습니다.

 

useMemo : 값의 메모이제이션

useMemo는 값의 계산 결과를 저장하여 불필요한 재계산을 방지하는 Hook입니다.

특히 연산량이 많은 작업을 반복해서 실행하지 않도록 도와줍니다.

import React, { useMemo } from 'react';

function ExpensiveCalculation({ num }) {
 const result = useMemo(() => {
  console.log('계산 중...');
  return num * 2;
 }, [num]); //num이 변경될 때만 계산
 
 return <p>결과: {result}</p>;
}
  • 연산량이 많은 작업(배열 정렬, 복잡한 계산 등)을 처리할 때 사용
  • 동일한 입력 값에 대해 결과가 변하지 않는 경우에 사용
  • 값을 캐싱하여 불필요한 재계산을 방지
  • 의존성 배열에 포함된 값이 변경될 때만 계산 함수가 실행
  • 주의 : 의존성 배열을 생략하면 매 랜더링마다 함수가 호출

* 메모이제이션(Memoization) : 계산의 결과를 저장해 두었다가, 같은 입력값이 들어오면 저장된 결과를 재사용하는 프로그래밍 기법

* 의존성 배열: Hook에서 사용되는 배열로, 어떤 값의 변화에 따라 Hook이 실행될지를 결정


useCallback: 함수를 메모이제이션

useCallback은 함수의 메모이제이션을 수행하는 Hook입니다. 랜더링 될 때마다 새로운 함수가 생성되는 것을 방지합니다.

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

function Counter() {
  const [count, setCount] = useState(0);

  const increment = useCallback(() => {
    setCount((prev) => prev + 1);
  }, []); // 의존성 배열이 비어 있으므로 초기 렌더링 시 한 번만 함수 생성

  return (
    <div>
      <p>카운트: {count}</p>
      <button onClick={increment}>증가</button>
    </div>
  );
}
  • 자식 컴포넌트에 함수를 전달해야 할 때 사용
  • 함수 내부에서 사용하는 값이 안정적으로 유지되어야 할 때 사용
  • 값이 아닌 함수를 반환
  • 의존성 배열에 포함된 값이 변경될 때만 새로운 함수가 생성

useRef : DOM 요소 또는 값 참조

useRef는 DOM 요소나 변경 가능한 값을 참조하기 위한 Hook입니다.

현재 값을 저장하지만, 값이 변경되어도 컴포넌트를 다시 렌더링하지 않습니다.

 

import React, { useRef } from 'react';

function FocusInput() {
  const inputRef = useRef(null);

  const handleFocus = () => {
    inputRef.current.focus(); // input DOM 요소에 직접 접근
  };

  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={handleFocus}>포커스</button>
    </div>
  );
}

 

  • DOM 요소에 접근할 때 사용
  • 상태와는 별개로 값을 저장할 수 있음
  • 값이 변경되더라도 컴포넌트를 다시 렌더링하지 않음

Summary

  • useMemo : 값의 재계산을 방지하여 성능을 최적화
  • useCallback : 함수의 재생성을 방지하여 불필요한 렌더링을 막음
  • useRef : DOM 요소나 변경 가능한 값을 참조하며, 컴포넌트의 렌더링에 영향을 미치지 않음

 

 

 

 

 

'REACT' 카테고리의 다른 글

[REACT] EVENT  (0) 2024.12.29
[React] Hook의 규칙 및 사용법  (1) 2024.12.24
[React] Hook : useState와 useEffect  (0) 2024.12.24
[React] State and Lifecycle : 리액트 컴포넌트의 관리  (2) 2024.12.03
[React] Components 와 Props  (1) 2024.11.26

React에서 Hook은 함수형 컴포넌트에서 상태와 생명주기 관련 기능을 사용할 수 있도록 도와주는 기능입니다.

 

useState : 상태 관리를 위한 Hook

useState는 함수형 컴포넌트에서 상태(State)를 관리하기 위한 Hook입니다.

React 클래스 컴포넌트의 this.state와 유사하지만, 훨씬 간결하게 사용할 수 있습니다.

import React, { useState } from 'react';

function Counter() {
 const [count, setCount] = useState(0);
 
 return (
  <div>
   <p>현재 카운트: {count}</p>
   <button onClick={() => setCount(count + 1)}>
    증가
   </button>
  </div>
 );
}

 

  • useState 함수는 배열을 반환합니다.
    • 첫 번째 요소 : 상태 값(예 : count)
    • 두 번째 요소 : 상태를 업데이트하는 함수(예 : setCount)
  • 초기 상태는 useState(초기값)의 인자로 전달합니다.
  • set함수명을 호출하면 상태가 업데이트되고 컴포넌트가 다시 랜더링 됩니다.

useEffect : 사이드 이펙트를 처리하기 위한 Hook

useEffect는 함수형 컴포넌트에서 사이드 이팩트를 수행하기 위한 Hook입니다.

사이드 이펙트란 데이터 가져오기, DOM 수정, 구독 설정 등의 작업을 의미합니다.

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

function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setSeconds((prev) => prev + 1);
    }, 1000);

    return () => clearInterval(interval); // 정리(clean-up) 함수
  }, []); // 의존성 배열이 비어 있으면 컴포넌트가 마운트될 때만 실행

  return <p>타이머: {seconds}초</p>;
}

 

  • useEffect(이펙트 함수, 의존성 배열) 형태로 사용됩니다.
  • 이펙트 함수 : 렌더링 후 실행될 작업을 정의합니다.
  • 의존성 배열 배열에 명시된 값이 변경될 때만 이펙트 함수가 실행됩니다.
    • 배열이 비어 있으면 컴포넌트가 처음 마운트될 때만 실행됩니다.
    • 의존성 배열을 생략하면 컴포넌트가 랜더링 될 때마다 실행됩니다.
  • 정리 함수 : 컴포넌트가 언마운트되거나 의존성이 변경될 때 호출됩니다.

* 의존성 배열에 포함되지 않은 값은 변경되더라도 이펙트 함수가 실행되지 않습니다.

* 무한 루프를 방지하려면 의존성 배열을 올바르게 설정해야 합니다.


Summary

React의 useState와 useEffect는 함수형 컴포넌트에서 상태 관리와 생명주기를 쉽게 처리할 수 있도록 도와주는 도구입니다. 

  • useState : 상태 값을 정의하고 관리
  • useEffect : 렌더링 후 작업 수행 및 정리 작업 처리

 

 

 

1. State(상태)

리액트에서 State는 컴포넌트 내에서 동적으로 변할 수 있는 데이터입니다. 이 값들은 렌더링이나 데이터 흐름에 사용되며, 컴포넌트의 UI를 제어합니다.

 

State의 특징

  • 변경 가능 : State는 시간이 지남에 따라 변경될 수 있는 값입니다. 예를 들어 사용자 입력, 서버에서 받은 데이터 등은 모두 State에 저장될 수 있습니다.
  • 불변성(Immutable) : State는 직접 수정해서는 안됩니다. 리액트는 State의 변화를 감지하여 컴포넌트를 다시 렌더링합니다. 따라서, State를 변경하려면 반드시 setState나 useState와 같은 메서드를 사용해야 합니다.
  • 리액트 컴포넌트에서의 사용 : 컴포넌트가 가진 상태 정보는 다른 컴포넌트와 분리되어 있으며, 이를 변경할 때마다 UI가 자동으로 갱신됩니다.
# State 사용 예시(함수형 컴포넌트)

function Counter() {
 // count 상태 변수를 선언하고, 이를 수정할 setCount 함수도 제공
 const [count, setCount] = useState(0);
 
 return (
  <div>
  	<p>현재 카운트 : {count}</p>
    <button onClick={() => setCount(count + 1)}>증가</button>
  </div>
 );
}

export default Counter;

 


2. Lifecycle(생명주기)

리액트 컴포넌트는 생명주기를 가지고 있으며, 컴포넌트는 시간의 흐름에 따라 생성, 업데이트, 삭제 됩니다. 각 단계마다 특정한 메서드가 호출됩니다. 리액트에서는 클래스 컴포넌트와 함수형 컴포넌트 모두 생명주기 메서드를 사용할 수 있지만, 함수형 컴포넌트에서는 주로 Hooks를 사용합니다.

* Hooks : 함수형 컴포넌트에서 상태 관리, 효과 처리, 컨텍스트 접근 등과 같은 React의 기능을 사용할 수 있도록 해주는 함수입니다. Hooks를 사용하면 클래스형 컴포넌트에서 제공되는 기능들을 함수형 컴포넌트에서도 쉽게 사용할 수 있습니다.

 

클래스 컴포넌트의 생명주기

출처 : https://gdsc-university-of-seoul.github.io/react-life-cycle/

리액트 클래스 컴포넌트의 생명주기는 크게 마운팅, 업데이트, 언마운팅 세 단계로 나눠집니다.

  1. 마운팅(Mounting) : 컴포넌트가 처음으로 DOM에 삽입되는 과정입니다. 
    • constructor() : 컴포넌트가 마운트된 후 호출됩니다.
    • componentDidMount() : 컴포넌트가 마운트 된 후 호출됩니다.
  2. 업데이트(Updating) : 업데이트가 필요한지 확인하는 메서드입니다.
    • shouldComponentUpdate() : 업데이트가 필요한지 확인하는 메서드입니다.
    • componentDidUpdate() : 업데이트가 완료된 후 호출됩니다.
  3. 언마운팅(Unmounting) : 컴포넌트가 더 이상 화면에 표시되지 않을 때 DOM에서 제거되는 과정입니다.
    • componentWillUnmount() : 컴포넌트가 언마운트되기 직전에 호출됩니다.
import React, { Component } from 'react';

class MyComponent extends Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  componentDidMount() {
    console.log('컴포넌트가 마운트되었습니다!');
  }

  componentDidUpdate(prevProps, prevState) {
    console.log('컴포넌트가 업데이트되었습니다!');
  }

  componentWillUnmount() {
    console.log('컴포넌트가 언마운트됩니다!');
  }

  render() {
    return (
      <div>
        <p>현재 카운트: {this.state.count}</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>증가</button>
      </div>
    );
  }
}

export default MyComponent;

3. 상태 변경과 컴포넌트 생명주기의 관계

컴포넌트의 State는 해당 컴포넌트의 생명주기 동안 변경될 수 있습니다. 상태가 변경되면, 리액트는 컴포넌트를 다시 렌더링 하고, 그에 맞는 UI를 갱신합니다. 이때 setState나 useState를 통해 상태를 변경하며, 이 변경에 따라 컴포넌트의 생명주기 메서드나 Hooks가 호출됩니다.


Summary

리액트에서 State는 컴포넌트의 데이터를 관리하는 중요한 역할을 하며, Lifecycle은 컴포넌트가 생성, 업데이트, 삭제되는 과정에서 호출되는 메서드입니다. 컴포넌트가 상태를 변경할 때마다 UI가 자동으로 업데이트되고, 이 과정은 생명주기 메서드나 Hook을 통해 관리됩니다.

 

'REACT' 카테고리의 다른 글

[React] Hook : useMemo, useCallback, useRef  (0) 2024.12.24
[React] Hook : useState와 useEffect  (0) 2024.12.24
[React] Components 와 Props  (1) 2024.11.26
[React] element란?  (0) 2024.11.25
[React] JSX란?  (0) 2024.11.25

+ Recent posts