리액트는 순수 함수를 이용하여 UI를 렌더링합니다.
즉 , 컴포넌트를 순수 함수로 구성하려합니다.
그렇다면 순수 함수는 무엇이고 왜 컴포넌트를 순수 함수로 구성하려하는 것일까요?
순수 함수는 동일한 인수를 전달하면 항상 동일한 결과를 반환하는 함수입니다. 이러한 특성 때문에 순수 함수는 부작용(side effect)을 일으키지 않으며, 예측 가능하고 테스트하기 쉽다는 장점이 있습니다.
순수 함수는 외부에서 전달받은 값을 변경하면 안됩니다. 즉 , 함수 내부에서 외부로 부작용(side effect)를 일으키면 안됩니다.
// 두 숫자를 더하는 함수
function add(a, b) {
return a + b;
}
// 배열을 정렬하는 함수
function sortArray(arr) {
return arr.sort();
}
// 객체를 복사하는 함수
function copyObject(obj) {
return { ...obj };
}
순수 함수의 특징은 동일한 입력에 대해 항상 같은 결과를 반환하며, 부작용(side effect)을 일으키지 않는다는 것입니다. 아래는 간단한 순수 함수의 예시입니다.
// 두 숫자를 더하는 함수
function add(a, b) {
return a + b;
}
// 배열을 정렬하는 함수
function sortArray(arr) {
return arr.sort();
}
// 객체를 복사하는 함수
function copyObject(obj) {
return { ...obj };
}
위의 예시 함수들은 모두 동일한 입력에 대해 항상 같은 결과를 반환하며, 부작용을 일으키지 않습니다. 이러한 특징 때문에 순수 함수는 예측 가능하고 테스트하기 쉽다는 장점이 있습니다.
리액트의 컴포넌트는 순수 함수와 유사한 특성을 가지고 있습니다. 컴포넌트는 props와 state를 입력받고, React 엘리먼트를 반환합니다.
이때 컴포넌트는 부작용을 일으키지 않고, 동일한 props와 state를 입력으로 받으면 항상 동일한 엘리먼트를 반환합니다.
리액트가 컴포넌트를 순수 함수로 구성하려는 이유는 다음과 같습니다.
예측 가능성
컴포넌트가 순수 함수로 작성되면, 동일한 props를 전달했을 때 항상 동일한 결과를 반환하기 때문에 예측 가능한 동작을 보장할 수 있습니다.
성능 최적화
컴포넌트가 순수 함수로 작성되면, props가 변경되지 않은 경우 렌더링을 건너뛸 수 있으므로 성능을 최적화할 수 있습니다.
테스트 용이성
순수 함수는 입력에 따라 항상 동일한 결과를 반환하기 때문에 테스트하기 쉽습니다.
유지보수성
부작용이 없는 순수 함수는 코드의 의도를 명확하게 표현할 수 있으므로 유지보수성이 높아집니다.
컴포넌트가 순수 함수로 작성되면, 상태 변화를 추적하기 쉽고 코드의 복잡성을 줄일 수 있습니다.
따라서 리액트에서는 순수 함수를 기반으로 하는 함수형 컴포넌트를 권장하고 있습니다.
사실 컴포넌트를 순수 함수로 만들지 않는다해서 코드가 동작하지 않는 것은 아닙니다.
하지만 리액트의 컴포넌트를 순수 함수로 만들지 않으면, 다음과 같은 문제점이 발생할 수 있습니다.
예측 불가능성
컴포넌트가 부작용을 일으키는 경우, props가 변경되지 않아도 예측할 수 없는 동작을 수행할 수 있습니다.
성능 저하
컴포넌트가 부작용을 일으키는 경우, props가 변경되지 않아도 렌더링을 수행해야 하므로 성능이 저하될 수 있습니다.
테스트 어려움
부작용이 있는 컴포넌트는 테스트하기 어려울 수 있습니다. 테스트 시 예상치 못한 결과가 발생할 수 있으며, 테스트 코드를 작성하기 어려울 수 있습니다.
유지보수성 저하
부작용이 있는 컴포넌트는 코드의 복잡성을 증가시킬 수 있으며, 상태 변화를 추적하기 어려울 수 있습니다.
따라서 리액트에서는 컴포넌트를 순수 함수로 만드는 것을 권장하고 있습니다.
그러나 순수 함수로 만들 수 없는 경우도 있습니다. 예를 들어, 외부 API 호출, 브라우저 이벤트 처리 등의 경우 부작용이 발생합니다. 이러한 경우에는 상태 관리 라이브러리를 사용하여 순수 함수로 만들어야 합니다.
다음은 리액트 컴포넌트를 순수 함수로 만들지 않은 예시들입니다.
import React, { useState, useEffect } from "react";
function ExampleComponent() {
const [data, setData] = useState(null);
useEffect(() => {
fetch("https://api.example.com/data")
.then((response) => response.json())
.then((data) => setData(data));
}, []);
return <div>{data ? data.message : "Loading..."}</div>;
}
순수 함수는 동일한 값을 반환해야하는데 외부 데이터에 의존하면 외부 데이터에 변경에 따라 다른 값을 반환하기 때문에 순수 함수에 원칙에 어긋납니다.
import React, { useState } from "react";
function ButtonComponent() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
};
return (
<div>
<button onClick={handleClick}>Click me</button>
<p>You clicked the button {count} times</p>
</div>
);
}
브라우저 이벤트는 예측할 수 없으며, 이벤트에 대한 처리는 언제나 다른 결과를 가져올 수 있습니다. 따라서 브라우저 이벤트에 의존하는 컴포넌트는 순수 함수 원칙을 어기게 됩니다.
위와 같이 외부 API를 호출하여 해당 데이터를 사용하는 컴포넌트나 브라우저 이벤트에 의존하는 컴포넌트는 순수 함수의 원칙에 어긋나므로 상태 관리 라이브러리로 관리해주면 됩니다.
그러면 외부 API를 호출하여 해당 데이터를 사용하는 컴포넌트나 브라우저 이벤트에 의존하는 컴포넌트를 상태 관리 라이브러리를 통하여 순수 함수로 유지시켜봅시다.
외부 데이터를 불러오는 API 호출 등의 부작용이 있기 때문에, 컴포넌트를 순수 함수로 만들 수 없습니다.
그래서 이러한 부작용을 상태 관리 라이브러리를 사용하여 분리하고, 컴포넌트는 단순히 상태 값을 받아와서 렌더링하는 역할만 수행하도록 만들 수 있습니다. 이를 위해 보통 Redux나 MobX와 같은 상태 관리 라이브러리를 사용합니다.
다음은 Redux를 사용하여 외부 API 호출을 분리하고, 순수 함수로 만들어진 컴포넌트를 보여주는 예시입니다.
import React, { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { fetchPosts } from "./actions";
function PostList() {
const posts = useSelector((state) => state.posts);
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchPosts());
}, [dispatch]);
return (
<div>
<h1>Post List</h1>
{posts.map((post) => (
<div key={post.id}>
<h3>{post.title}</h3>
<p>{post.body}</p>
</div>
))}
</div>
);
}
export default PostList;
위 코드에서 fetchPosts는 Redux action creator 함수입니다.
useEffect를 사용하여 컴포넌트가 마운트되면 fetchPosts 액션을 dispatch하여 API에서 데이터를 가져옵니다.
그리고 useSelector를 사용하여 Redux store에서 posts 상태 값을 가져와 렌더링합니다.
이렇게 함으로써 PostList 컴포넌트는 부작용을 상태 관리 라이브러리를 통해 분리하고, 순수 함수로 유지할 수 있습니다.
브라우저 이벤트에 의존하는 컴포넌트를 상태 관리 라이브러리를 통해 순수 함수로 유지하기 위해서는 외부 상태를 변경하는 부분을 모두 상태 관리 라이브러리로 옮겨야 합니다.
아래는 React Hooks와 Redux를 사용하여 브라우저 이벤트에 의존하는 컴포넌트를 상태 관리 라이브러리를 통해 순수 함수로 유지하는 예시 코드입니다.
import React, { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { incrementCount } from "../actions/counterActions";
const Counter = () => {
const dispatch = useDispatch();
const count = useSelector((state) => state.counter);
const handleClick = () => {
dispatch(incrementCount());
};
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>Increment</button>
</div>
);
};
export default Counter;
위 예시 코드는 useSelector를 사용하여 Redux store에서 상태 값을 가져오고, useDispatch를 사용하여 액션을 디스패치하여 상태를 업데이트합니다.
dispatch는 특정 값을 변경해주는 것이 아닌 특정 액션값을 보내줍니다.
그리고 특정 액션이 dispatch되면 리듀서가 작용하여 해당 액션에 대한 로직을 구현합니다.
이는 store 내에서 로직이 이루어지는 것이므로 컴포넌트 자체는 순수 함수로 유지할 수 있게 합니다.
이를 통해 브라우저 이벤트에 의존하는 컴포넌트를 순수 함수로 유지할 수 있습니다.
리액트 함수형 컴포넌트는 순수 함수로 유지하지 않아도 동작은 합니다.
하지만 예측 가능한 컴포넌트 함수를 만들어 여러 가지 장점을 얻기 위해서는 컴포넌트 순수 함수로 만들려고 하는 것이 좋습니다.
앞으로는 컴포넌트를 구성할 때 해당 컴포넌트가 순수 함수인가를 한번씩 생각해보고 이를 해결하기 위해서는 위와 같은 상태 관리 라이브러리를 적용해보도록 해야겠습니다.