React에도 디자인 패턴이 있다고?
2024.11.24- -
1. 들어가며
소프트웨어쪽 공부를 하다보면 디자인 패턴이나 아키텍처에 대해 많이들 들어보셨을 겁니다.
사실 디자인 패턴이 많이 사용되는 분야는 백엔드이지만(싱글톤, 팩토리, 빌더, .....) 프론트엔드도 마찬가지로 이와 비슷한 고민으로부터 나온 몇가지 디자인 패턴이 존재한다는 사실 알고 계셨나요?
디자인 패턴이란?
디자인 패턴은 소프트웨어 개발의 일반적인 문제에 대한 표준화된 솔루션을 제공합니다.
React에서의 디자인 패턴
React에서 디자인 패턴을 활용하면 확장성, 유지보수성, 효율성 측면에서 더욱 뛰어난 애플리케이션을 만들 수 있습니다.
이번 포스팅에서는 컨테이너 & 프레젠테이션 컴포넌트 패턴(Container and Presentational Components Pattern), 고차 컴포넌트 패턴(Higher-Order Component), 렌더 속성 패턴(Render Props Pattern), 커스텀 훅 패턴(Custom Hook Pattern) 등 React의 몇 가지 필수 디자인 패턴들에 대해서 살펴볼 것입니다. 각 패턴에 대해서는 모던 리액트 훅을 사용하여 설명하도록 할 것입니다.
2. 컨테이너 & 프레젠테이셔널 컴포넌트 패턴(Container and Presentational Component Pattern)
이 패턴은 컴포넌트를 state나 로직을 처리하는 컨테이너 컴포넌트(똑똑한 컴포넌트)와 렌더링 UI를 처리하는 프레젠테이션 컴포넌트(멍청한 컴포넌트)의 두 가지 카테고리로 나누어 구분합니다.
이렇게 분리함으로써 얻을 수 있는 이점은 다음과 같은 것들이 있습니다.
- 관심사의 분리(Separation of Concerns): 비즈니스 로직을 UI 렌더링과 분리합니다.
- 재사용성(Reusability): 프레젠테이션 컴포넌트를 애플리케이션 여러 부분에서 재사용할 수 있습니다.(데이터 로직이 다르더라도)
그렇다면 구현 모습을 한 번 봐볼까요?
컨테이터 컴포넌트 (Container Component)
// TodoContainer.js
import React, { useState, useEffect } from 'react';
import TodoList from './TodoList';
const TodoContainer = () => {
const [todos, setTodos] = useState([]);
useEffect(() => {
fetch('/api/todos')
.then(response => response.json())
.then(data => setTodos(data));
}, []);
return <TodoList todos={todos} />;
};
export default TodoContainer;
사실 저는 이러한 컨테이너 컴포넌트에 대해서 잘 모를 때, hooks 관련된 코드가 파일에 너무 많아지다보니 따로 분리를 시켰었는데 이것이 바로 컨테이너라고 한다는 사실을 알게되었습니다. 하지만 hooks와 관련된 코드이기 때문에 커스텀 훅인가? 해서 해당 컴포넌트 앞에 'use' prefix를 붙여주곤 했는데 이 컨테이너 컴포넌트는 반환값이 데이터가 아닌 JSX이기 때문에 'use'가 붙는 것이 아니라 이 패턴의 feature인 'container'를 suffix로 붙여주는 것이 인상적입니다.
프레젠테이션 컴포넌트(Presentational Component)
// TodoList.js
import React from 'react';
const TodoList = ({ todos }) => (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
export default TodoList;
그렇다면 이 프레젠테이션 컴포넌트는 당연히 앞서 컨테이너 컴포넌트의 반환값으로 사용된 JSX의 구현체가 되는 것입니다.
3. Higher-Order Component(HOC) Pattern - 고차 컴포넌트 패턴
HOC는 컴포넌트를 받아서 추가적인 props나 행동이 포함된 새로운 컴포넌트를 반환하는 함수라고 보면 됩니다.
이를 사용함으로써 얻을 수 있는 이점은 다음과 같습니다.
- 코드 재사용성(Code Resuability): 여러 컴포넌트에서 공유할 수 있는 공통된 동작은 캡슐화합니다.
- 기능의 확장(Enhancement): 추가 기능으로 컴포넌트를 업그레이드 시킬 수 있습니다.
이것도 구현 모습을 살펴보겠습니다.
// withLogging.js
import React, { useEffect } from 'react';
const withLogging = WrappedComponent => {
return props => {
useEffect(() => {
console.log('Component mounted');
}, []);
return <WrappedComponent {...props} />;
};
};
export default withLogging;
HOC에서는 'with'라는 prefix가 붙는 컴포넌트를 만드는데 이것이 고차 컴포넌트입니다. 눈에 띄는 것은 withLogging에서는 어떠한 컴포넌트를 인자로 받아 내부에서 해당 컴포넌트 위에 다시 여러겹 감싸서 반환하게됩니다.
위 예시에서는 useEffect로 로그를 한 번 찍는 '행동'이 추가가 되었네요.
// MyComponent.js
import React from 'react';
import withLogging from './withLogging';
const MyComponent = () => <div>Hello, World!</div>;
export default withLogging(MyComponent);
이를 사용할 때는 감싸고 싶은 어떤 컴포넌트에 대해 withLogging 함수의 인자로 넣어주어 export 하도록 하면 됩니다.
이렇게 되면 MyComponent는 div 태그 하나를 반환하는 단순한 컴포넌트에서 내부적으로 로그를 한 번 찍는 보다 기능이 확장된 컴포넌트가 되었네요.
4. Render Props Pattern
이 패턴은 컴포넌트 간에 데이터를 공유하는 함수인 prop을 사용합니다.
이 패턴의 이점은 다음과 같습니다.
- 유연성(Flexibility): 동적인 동작을 컴포넌트에 전달하는 방법을 제공합니다.
- 재사용성(Reusability): 함수를 props으로 전달하여 코드의 재사용성을 부추깁니다.
여기까지 봤을 때 React에서 패턴이 갖는 이점은 다 거기서 거기인 것을 볼 수 있습니다. 사실 디자인 패턴의 목적은 이러한 것들을 크게 벗어나지 않으며, react에서도 이러한 주된 목적을 기반으로 여러 패턴이 나온 것을 볼 수 있는 것입니다.
그렇다면 Render Props 패턴 구현 모습을 봅시다.
// MouseTracker.js
import React, { useState } from 'react';
const MouseTracker = ({ render }) => {
const [position, setPosition] = useState({ x: 0, y: 0 });
const handleMouseMove = event => {
setPosition({
x: event.clientX,
y: event.clientY
});
};
return (
<div style={{ height: '100vh' }} onMouseMove={handleMouseMove}>
{render(position)}
</div>
);
};
export default MouseTracker;
render라는 속성을 props로 받아 내부에서 사용되도록 되어 있는데 코드를 보니 render는 함수인 것 같습니다. 이 함수는 데이터를 받아 컴포넌트의 특정 UI를 정의할 것임을 예측해볼 수 있습니다.
// App.js
import React from 'react';
import MouseTracker from './MouseTracker';
const App = () => (
<MouseTracker render={({ x, y }) => (
<h1>The mouse position is ({x}, {y})</h1>
)} />
);
export default App;
코드가 조금 복잡할 수 있는데 render라는 props으로 함수 `({ x, y }) = ...`를 전달하고 있습니다. 이 전달된 함수는 position 상태를 받아 UI를 렌더링하게 됩니다.
App 컴포넌트에서 MouseTracker를 사용하여 렌더링 로직을 전달하고 render라는 함수는 MouseTracker로부터 전달받은 position 상태를 기반으로 UI를 렌더링하는 것입니다.
이 패턴의 목적은 컴포넌트 간에 재사용 가능한 로직을 공유하는 것에 초점을 맞춰져 있다고 볼 수 있습니다. 함수(render)를 props로 전달함으로써 컴포넌트가 어떻게 렌더링할지를 제어할 수 있도록 합니다.
5. Custom Hook Pattern - 커스텀 훅 패턴
커스텀 훅 패턴을 사용하면 로직과 상태 관리를 재사용 가능한 함수로 캡슐화할 수 있습니다.
- 코드 재사용성(Code Reusability): 여러 컴포넌트에서 로직을 추출하여 재사용할 수 있습니다
- 단순화(Simplicification): 복잡한 로직을 훅으로 이동하여 컴포넌트 로직부분을 단순화 시킵니다.
// useFetchData.js
import { useState, useEffect } from 'react';
const useFetchData = url => {
const [data, setData] = useState(null);
useEffect(() => {
fetch(url)
.then(response => response.json())
.then(data => setData(data));
}, [url]);
return data;
};
export default useFetchData;
// DataDisplay.js
import React from 'react';
import useFetchData from './useFetchData';
const DataDisplay = ({ url }) => {
const data = useFetchData(url);
return data ? <pre>{JSON.stringify(data, null, 2)}</pre> : <p>Loading...</p>;
};
export default DataDisplay;
이것에 제가 앞서 사용했다 말했던 커스텀 훅 패턴입니다. 컨테이너 프레젠테이션 패턴과 유사해보이지만 'use'라는 prefix가 붙는 커스텀 훅에서 반환하는 값은 상태 혹은 핸들러가 됩니다.
사실 재사용되지 않을 훅이라면 커스텀 훅 패턴 보다는 컨테이너 프레젠테이션 패턴을 사용하는 것이 맞지 않나라는 생각이 듭니다.
6. Compound Component Pattern - 컴파운드 컴포넌트 패턴
컴파운드 컴포넌트 패턴을 사용하면 컴포넌트를 함께 구성하여 각 컴포넌트가 더 큰 목적에 기여하므로 보다 응집력 있는 단위를 형성할 수 있습니다.
이젠 뻔하지만 이점은 다음과 같습니다.
- 캡슐화(Encapsulation): 컴포넌트는 각자의 책임을 유지하면서도 원활하게 함께 작동합니다.
- 유연성(Flexibility): 사용자는 컴파운드 컴포넌트의 동작과 모양을 부품을 통해 커스터마이징할 수 있습니다.
// Toggle.js
import React, { useState } from 'react';
const Toggle = ({ children }) => {
const [on, setOn] = useState(false);
const toggle = () => {
setOn(!on);
};
const getChildProps = () => {
return {
on,
toggle,
};
};
return <>{React.Children.map(children, child => React.cloneElement(child, getChildProps()))}</>;
};
export default Toggle;
컴파운드 패턴을 통해 상태(on / off)를 관리하고, 자식 컴포넌트에게 토글 기능을 제공합니다.
// ToggleButton.js
import React from 'react';
const ToggleButton = ({ on, toggle }) => (
<button onClick={toggle}>{on ? 'Turn Off' : 'Turn On'}</button>
);
export default ToggleButton;
클릭 이벤트를 처리하고 적절한 텍스트를 표시하기 위해 'on' 상태와 toggle 핸들러를 props로 수신합니다.
// ToggleMessage.js
import React from 'react';
const ToggleMessage = ({ on }) => (
<p>{on ? 'The button is ON' : 'The button is OFF'}</p>
);
export default ToggleMessage;
현재 상태에 따라 메시지를 표시하기 위해 'on' 상태를 수신합니다.
// App.js
import React from 'react';
import Toggle from './Toggle';
import ToggleButton from './ToggleButton';
import ToggleMessage from './ToggleMessage';
const App = () => (
<Toggle>
<ToggleButton />
<ToggleMessage />
</Toggle>
);
export default App;
이러한 패턴 방식은 ToggleButton과 ToggleMessage가 Toggle의 관리 하에 원활하게 함께 작동하여 복합 컴포넌트 패턴의 캡슐화 및 유연성을 보여줄 수 있게 됩니다.
7. 마치며
이렇게 React에서 사용되는 디자인 패턴들에 대해서 살펴봤는데 대부분의 라이브러리들 코드를 봤을 때 자주 봤던 패턴들이 있어 이런 목적으로 사용된 거구나라는 것을 깨달을 수 있었고 보다 깔끔한 구조를 위해 적절한 패턴을 적용하면 좋을 것 같습니다.
'Front-end' 카테고리의 다른 글
Access Token을 브라우저에 저장 시 고려해야 할 사항들 (0) | 2024.11.27 |
---|---|
BFF란 무엇인가 (0) | 2024.11.25 |
Headless란 무엇인가? (1) | 2024.11.23 |
[TypeScript] enum vs. const (0) | 2024.11.22 |
axios vs. fetch (0) | 2024.11.21 |
소중한 공감 감사합니다