요즘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 |