리액트는 컴포넌트라는 개념을 도입함. 이는 코드 재사용성을 높이고 유지보수를 편리하게 해줌. 각 컴포넌트는 독립적으로 동작하고 조합 가능해 복잡한 UI를 쉽게 구축할 수 있음.
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
function App() {
return (
<div>
<Welcome name="Alice" />
<Welcome name="Bob" />
<Welcome name="Charlie" />
</div>
);
}
리액트는 Virtual DOM을 사용함. DOM 조작은 느리고 비효율적인 작업인데, 리액트는 이를 최적화함. 이는 불필요한 렌더링을 최소화하여 성능을 향상시킴.
리액트의 데이터는 항상 위에서 아래로(부모에서 자식으로) 흐름. 이로 인해 앱의 동작을 예측하기 쉬움.
리액트 자체는 상대적으로 간단하지만, 리액트와 함께 사용하는 기술들(예: Redux, Router)을 익히는 것은 어려울 수 있음.
리액트는 라이브러리로서, 전체 애플리케이션을 구축하는 데 필요한 모든 기능을 제공하지 않음. 상태 관리나 라우팅 등 추가 기능을 위해서는 외부 라이브러리가 필요함.
참고로 CRA(Create-React-App)을 사용하면, 본적으로 리액트 라이브러리와 리액트 DOM, 테스트 라이브러리, 웹팩, Babel 등 필요한 환경을 제공함. 다만 React Router나 Redux와 같은 라이브러리를 기본적으로 포함하고 있지 않음. 필요에 따라 개발자가 추가로 설치하여 사용해야 함.
리액트의 성능은 기본적으로 우수하지만, 큰 규모의 애플리케이션에서는 추가적인 최적화가 필요할 수 있음. 가상 DOM 또한 비용이 들고, 이에 대한 이해가 필요함.
리액트의 라이브러리로서의 한계를 극복하기 위해 다양한 툴과 라이브러리를 사용할 수 있음. 주로 사용되는 몇 가지를 소개함.
Redux : 가장 널리 사용되는 상태 관리 라이브러리 중 하나임. 애플리케이션의 전체 상태를 관리하는데 유용하고, 시간여행 디버거 등 고급 디버깅 기능을 제공함.
MobX : 간단하고 빠른 방법으로 애플리케이션 상태를 관리할 수 있음. 'reactive'한 상태 관리를 제공해 자동으로 최소한의 업데이트를 수행함.
Context API : 리액트 자체에서 제공하는 상태 관리 기능임. Redux나 MobX보다 간단한 상황에서 사용하기 적합함.
React Router : 리액트 애플리케이션에 라우팅 기능을 제공하는 가장 대표적인 라이브러리임. 웹 브라우저에서의 페이지 전환 효과를 쉽게 구현할 수 있음.
Next.js : Next.js는 서버 사이드 렌더링(SSR)과 정적 사이트 생성을 지원하는 리액트 프레임워크임. 자체적인 라우팅 시스템을 가지고 있어 별도의 라우팅 라이브러리가 필요 없음.
Reach Router : Reach Router는 리액트를 위한 라우팅 라이브러리로, 접근성에 중점을 두고 있음. 간단하고 예측 가능한 라우팅을 제공함.
Gatsby : Gatsby는 정적 사이트 생성기로서, 리액트 기반임. 동적 라우팅을 지원하며, 페이지 생성 시에 미리 경로를 정의함.
리액트에서 컴포넌트는 UI의 독립적인, 재사용 가능한 부분을 나타냄. 각각의 컴포넌트는 자체적인 마크업과 로직을 가짐.
간단한 예제로 Button
컴포넌트를 만들어 보겠음.
// Button.js
import React from 'react';
function Button(props) {
return (
<button>{props.label}</button>
);
}
export default Button;
이 Button
컴포넌트는 재사용 가능하고 독립적으로 동작함. label
이라는 props를 통해 버튼의 텍스트를 지정할 수 있음. 이 컴포넌트를 사용하려면 다음과 같이 사용하면 됨.
// App.js
import React from 'react';
import Button from './Button';
function App() {
return (
<div>
<Button label="Click Me!" />
<Button label="Another Button" />
</div>
);
}
export default App;
위의 코드에서 Button
컴포넌트는 두 번 사용되었지만, 각각 다른 라벨을 가짐. 이처럼 컴포넌트는 재사용이 가능하고, 필요에 따라 다르게 동작하도록 설정할 수 있음.
useState
는 리액트의 Hook 중 하나로, 컴포넌트의 상태 관리를 가능하게 함. useState
를 이용하면 함수형 컴포넌트 내에서 상태를 가질 수 있게 됨.
간단한 예제로 버튼을 클릭할 때마다 카운트가 증가하는 Counter
컴포넌트를 만들어 보겠음.
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Clicked {count} times</p>
<button
// 이전 상태를 직접 참조하는 방법
onClick={() => setCount(count + 1)}
>
Click me
</button>
{/* 이전 상태를 함수를 통해 참조하는 방법도 있음. 이 방법이 더 안전함.
<button onClick={() => setCount((prevCount) => prevCount + 1)}>
Click me
</button>
*/}
</div>
);
}
export default Counter;
여기서 useState(0)
을 호출하면 초기값 0을 가진 상태 변수 count
와 해당 상태를 변경하는 함수 setCount
가 반환됨. 이들을 배열 비구조화 할당 (배열 구조분해라고도 함. Array Destructuring. 위의 [count, setCount] 부분) 으로 추출함.
버튼을 클릭하면 onClick
핸들러 함수가 실행되고, setCount
함수를 통해 count
상태 값이 1 증가함. 이렇게 상태가 변경되면 리액트는 해당 컴포넌트를 다시 렌더링하고, 업데이트된 count
값을 화면에 표시함.
리액트에서 데이터 흐름은 상위에서 하위로 이루어짐. 이를 단방향 데이터 흐름(One-way data flow) 또는 top-down 또는 unidirectional 데이터 흐름이라 함. 이는 상위 컴포넌트가 하위 컴포넌트에게 props를 통해 데이터를 전달하고, 이 데이터는 읽기 전용이라는 원칙을 가지고 있음.
그런데 경우에 따라서는 자식 컴포넌트에서 생성된 데이터를 상위 컴포넌트로 전달해야 할 필요가 생김. 이때 사용하는 기법이 '상태 끌어올리기(Lifting State Up)'임.
상태 끌어올리기란, 공유되어야 하는 state를 가장 가까운 공통 상위 컴포넌트로 이동시키는 것을 말함. 그리고 이 상위 컴포넌트에서 상태 변경 함수를 하위 컴포넌트로 전달하여 상태를 변경하게 함.
다음은 상태 끌어올리기의 간단한 예제임.
import React, { useState } from 'react';
// 자식 컴포넌트. 상위 컴포넌트로부터 value와 onValueChange를 props로 받음
function Child({ value, onValueChange }) {
const handleChange = (event) => {
onValueChange(event.target.value);
};
// 입력 컴포넌트. 상위 컴포넌트의 상태를 value로, 상태 변경 함수를 onChange 핸들러로 사용함
return <input value={value} onChange={handleChange} />;
}
// 상위 컴포넌트. 자식 컴포넌트의 입력값 상태를 관리함
function App() {
const [value, setValue] = useState("");
// 자식 컴포넌트에 상태와 상태 변경 함수를 props로 전달함
// p 태그를 사용하여 현재 입력값 상태를 보여줌
return (
<div>
<Child value={value} onValueChange={setValue} />
<p>The input value is: {value}</p>
</div>
);
}
export default App;
참고로 아래 코드 예제는 위의 코드 예제와 동일하게 작동함. 화살표 함수를 이용하고, 자식 컴포넌트에 props를 전달하는 방식이 다를 뿐임.
import React, { useState } from 'react';
const Child = (props) => {
const handleChange = (event) => {
props.onValueChange(event.target.value);
};
return <input value={props.value} onChange={handleChange} />;
}
const App = () => {
const [value, setValue] = useState("");
return (
<div>
<Child value={value} onValueChange={setValue} />
<p>The input value is: {value}</p>
</div>
);
}
export default App;
여기서 App
컴포넌트는 상태 value
와 해당 상태를 변경하는 setValue
함수를 가지고 있음. 이 두 값을 Child
컴포넌트에 props로 전달함. Child
컴포넌트에서는 input
의 onChange
이벤트를 통해 setValue
함수를 호출하여 value
를 변경함. 이렇게 하면 Child
컴포넌트에서 생성된 입력값이 App
컴포넌트로 전달됨.
Hooks는 리액트 16.8에서 도입된 기능으로, 함수형 컴포넌트에서도 상태 관리와 라이프사이클 기능을 사용할 수 있게 해줌. 기본적인 몇 가지 Hooks를 알아보겠음.
useState
는 컴포넌트 내에서 상태를 가지고 있게 해주는 Hook임. 컴포넌트의 로컬 상태를 선언하고, 관리할 수 있게 해줌.
const [state, setState] = useState(initialState);
useEffect
는 사이드 이펙트를 수행하는 Hook임. 데이터 가져오기, 구독 설정 및 해제, 수동으로 리액트 컴포넌트의 DOM을 수정하는 작업 등, 컴포넌트가 렌더링 이후에 수행되어야 하는 작업을 관리함.
useEffect(() => {
// side effect를 수행하는 코드
}, [dependencies]);
useContext
는 컴포넌트를 중첩하지 않고도 React 컨텍스트를 구독할 수 있게 해주는 Hook임.
const value = useContext(MyContext);
useReducer
는 복잡한 컴포넌트 상태 로직을 관리하거나, 상태를 구성요소 트리로 전달해야 할 때 useState
보다 우수한 대안이 될 수 있는 Hook임.
const [state, dispatch] = useReducer(reducer, initialState);
useRef
는 .current 프로퍼티로 전달된 인자(initialValue)를 가리키는 변경 가능한 ref 객체를 반환함.
const refContainer = useRef(initialValue);
각각의 Hooks는 서로 다른 문제를 해결하는데 사용되며, 함께 사용하면 더 강력한 기능을 구현할 수 있음.
끝.