-
ReactJS의 디자인 패턴에 대해서 알아보자!Front-end/React 2024. 5. 22. 09:51반응형
개발 공부를 하다보면 디자인 패턴이라는 것에 대해서 들어봤을 것이다.
디자인 패턴이라는 것은 반복적으로 일어나는 문제들을 어떻게 풀어나갈 것인가에 대한 재사용 가능한 솔루션을 제공해놓은 것을 말한다.
대표적으로는 <GoF의 디자인패턴>이라는 것이 있다.
그렇다면 React를 주로 사용하는 프론트엔드 개발자에게 있어서 사용할 수 있는 디자인패턴은 어떤 것들이 있을까?
HOC(고차 컴포넌트)
우선 HOC이다. HOC는 자주 들어봤을 것이라고 생각한다. 이는 컴포넌트 자체를 인수로 받아들이고 향상된 기능을 갖춘 새 컴포넌트를 반환하는 함수이다.
추가 기능으로 컴포넌트를 감싸서 코드 재사용, 교차 편집 문제 및 동작 구성을 가능하게 한다.
HOC를 이용한 authentication, data fetching, memoization 등을 할 수 있다.
import React from 'react'; const withLoading = (Component) => { return function WithLoadingComponent({ isLoading, ...props }) { if (isLoading) { return <div>Loading...</div>; } return <Component {...props} />; }; }; export default withLoading;
import React from 'react'; const ComponentToEnhance = ({ data }) => { return ( <div> {data.map(item => ( <div key={item.id}>{item.name}</div> ))} </div> ); }; export default ComponentToEnhance;
import React from 'react'; import withLoading from './withLoading'; import ComponentToEnhance from './ComponentToEnhance'; const EnhancedComponent = withLoading(ComponentToEnhance); const App = () => { const data = [ { id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' }, ]; const isLoading = false; return <EnhancedComponent isLoading={isLoading} data={data} />; }; export default App;
Container-Component Pattern
다른 이름으로 "Smart-Dumb 컴포넌트 패턴" 이라고도 불리는데, 이름 그대로 상태나 로직을 관리하는 컴포넌트인 "Smart component"와 UI 렌더링에 중점을 둔 "Presentation component(Dumb component)"로 분리하는 것이다.
이렇게 로직과 UI를 분리함으로써 재사용성과 유지관리성을 향상 시킬 수 있다.
import React, { useState, useEffect } from 'react'; // ContainerComponent.jsx const PresentationalComponent = ({ data, onClick }) => { return ( <div> {data.map(item => ( <div key={item.id} onClick={() => onClick(item.id)}> {item.name} </div> ))} </div> ); }; // ContainerComponent.jsx const ContainerComponent = () => { const [data, setData] = useState([]); useEffect(() => { // 예시 데이터를 비동기적으로 가져오는 로직 fetchData().then(fetchedData => setData(fetchedData)); }, []); const handleItemClick = (id) => { console.log(`Item clicked: ${id}`); }; return <PresentationalComponent data={data} onClick={handleItemClick} />; }; const fetchData = async () => { // 예시 데이터 return [ { id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' }, ... ]; };
Render Props
props로 함수를 전달하고 그 함수를 통해 UI를 렌더링하는 방식이다.
import React, { useState, useEffect } from 'react'; const DataProvider = ({ render }) => { const [data, setData] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { fetchData().then(fetchedData => { setData(fetchedData); setLoading(false); }); }, []); return render({ data, loading }); }; const fetchData = async () => { return [ { id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' }, ]; }; export default DataProvider;
import React from 'react'; import DataProvider from './DataProvider'; const ComponentUsingRenderProps = () => { return ( <DataProvider render={({ data, loading }) => { if (loading) { return <div>Loading...</div>; } return ( <div> {data.map(item => ( <div key={item.id}>{item.name}</div> ))} </div> ); }} /> ); }; export default ComponentUsingRenderProps;
Context API
그래도 리액트를 사용하는 사람들이라면 prop drilling과 상태관리에 대해서 고민하고 있을 것이다.
Context API를 사용하면 컴포넌트 트리를 통해 Property를 수동으로 전달하지 않고도 컴포넌트가 전역 상태를 공유할 수 있게 된다.
모든 수준(레벨)에서 명시적으로 porps를 전달하지 않고도 컴포넌트 트리를 통해 데이터를 전달하는 방법을 제공한다.
큰 데이터의 경우에는 Redux, mobX 등을 이용하기도 하지만 어플리케이션의 전체 상태, 테마, 사용자 환경설정 등을 설정 및 관리할 때 유용하게 사용할 수 있다.
import React, { createContext, useContext, useState } from 'react'; const MyContext = createContext(); export const useMyContext = () => useContext(MyContext); export const MyProvider = ({ children }) => { const [value, setValue] = useState('default value'); return ( <MyContext.Provider value={{ value, setValue }}> {children} </MyContext.Provider> ); };
import React from 'react'; import { MyProvider } from './MyContext'; import ComponentUsingContext from './ComponentUsingContext'; const App = () => { return ( <MyProvider> <ComponentUsingContext /> </MyProvider> ); }; export default App;
import React from 'react'; import { useMyContext } from './MyContext'; const ComponentUsingContext = () => { const { value, setValue } = useMyContext(); return ( <div> <div>Value: {value}</div> <button onClick={() => setValue('new value')}>Change Value</button> </div> ); }; export default ComponentUsingContext;
Compound Components
여러 자식 컴포넌트들의 조합으로 기능을 확장하는 패턴을 말한다.
관련 된 여러 컴포넌트를 함께 제공하여 유연하게 사용하는데 목적을 두고 있다.
주로 탭 인터페이스, 메뉴 아코디언, 폼 컨트롤 등에 사용된다.
import React, { createContext, useContext, useState } from 'react'; const ToggleContext = createContext(); const Toggle = ({ children }) => { const [on, setOn] = useState(false); const toggle = () => setOn(!on); return ( <ToggleContext.Provider value={{ on, toggle }}> {children} </ToggleContext.Provider> ); }; const ToggleOn = ({ children }) => { const { on } = useContext(ToggleContext); return on ? children : null; }; const ToggleOff = ({ children }) => { const { on } = useContext(ToggleContext); return on ? null : children; }; const ToggleButton = () => { const { toggle } = useContext(ToggleContext); return <button onClick={toggle}>Toggle</button>; }; Toggle.On = ToggleOn; Toggle.Off = ToggleOff; Toggle.Button = ToggleButton; export default Toggle;
import React from 'react'; import Toggle from './Toggle'; const App = () => { return ( <Toggle> <Toggle.On> <div>The toggle is ON</div> </Toggle.On> <Toggle.Off> <div>The toggle is OFF</div> </Toggle.Off> <Toggle.Button /> </Toggle> ); }; export default App;
State Management Patterns
React는 복잡한 상태 및 데이터 흐름을 관리하기 위해서 Redux, MobX, zustand, recoil 등 다양한 전역 관리 라이브러리와 Context API 같은 것들을 사용하는 경우가 많다.
이러한 패턴들은 중앙 집중식 상태 관리, 예측 가능한 데이터 흐름, 관심사 분리를 제공하므로 대규모 애플리케이션에서 상태관리를 용이하게 할 수 있게 도와준다.
Immutable Data Patterns
Immutable이라고하는 것은 "불변"을 말한다. 즉, 변하지 않는다는 뜻이다.
불변 데이터 구조와 functional programming(함수형 프로그래밍) 원칙을 사용하여 React 어플리케이션에서 상태 업데이트를 관리하도록 권장한다.
불변성을 유지한다는 것은 데이터를 추적하고 상태를 예측가능하게 만들며, 성능을 최적화 하는데 도움을 줄 수 있는 것이다.
그래서 Immutable.js 혹은 Immer와 같은 라이브러리를 사용해서 불변 데이터 구조를 생성하고 업데이트하여 상태 관리의 버그를 줄일 수 있다.
import { Map } from 'immutable'; const state = Map({ count: 0, }); const newState = state.set('count', state.get('count') + 1); console.log(state.get('count')); // 0 console.log(newState.get('count')); // 1
import produce from 'immer'; const state = { count: 0, }; const newState = produce(state, draft => { draft.count += 1; }); console.log(state.count); // 0 console.log(newState.count); // 1
Error Boundary Pattern
Error boundaries는 하위 컴포넌트 트리의 어느 곳에서나 자바스크립트 에러를 포착하고 전체 어플리케이션이 unmount 되는 대신 fallback UI를 표시하게 하는 컴포넌트이다.
일반적으로 자식 컴포넌트에서 exception이 발생하면 부모 컴포넌트까지 unmount되면서 하얀 화면을 보게 되는 경우가 많다.
그러나 만약 컴포넌트가 나뉘어진 상태라면 굳이 모든 화면을 다 하얗게 만들어 UX를 떨어트릴 필요가 없을 것이다.
이런 경우 각 컴포넌트 별로 Error boundary를 걸어놓으면 그 컴포넌트만 fallback 처리가 되고 나머지 UI는 그대로 살아있어서 UX에 도움을 줄 수 있다.
ErrorBoundary를 만들고자 한다면 class를 사용해야한다.
만약 class가 익숙하지 않다면 라이브러리도 있으니 더 쉽게 함수형 컴포넌트에서 사용할 수 있을 것이다.import React, { Component } from 'react'; class ErrorBoundary extends Component { constructor(props) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(error) { return { hasError: true }; } componentDidCatch(error, errorInfo) { console.error("Error caught in ErrorBoundary:", error, errorInfo); } render() { if (this.state.hasError) { return <div>Something went wrong.</div>; } return this.props.children; } } export default ErrorBoundary;
import React from 'react'; import ErrorBoundary from './ErrorBoundary'; import ComponentThatMayError from './ComponentThatMayError'; const App = () => { return ( <ErrorBoundary> <ComponentThatMayError /> </ErrorBoundary> ); }; export default App;
State Reducer
복잡한 상태관리를 단순화 하는데 사용되며 주로 useReducer 훅과 함께 사용되어 액션에 따라 상태를 업데이트하게 된다.
리덕스를 사용해본 분이라면 reducer에 대해서 익숙하게 느껴질 것이다.
이 패턴은 Redux와 함께 사용되기 때문이다.
const initialState = { count: 0 }; const counterReducer = (state, action) => { switch (action.type) { case 'INCREMENT': return { count: state.count + 1 }; case 'DECREMENT': return { count: state.count - 1 }; default: return state; } }; export { initialState, counterReducer };
import React, { useReducer } from 'react'; import { initialState, counterReducer } from './counterReducer'; const Counter = () => { const [state, dispatch] = useReducer(counterReducer, initialState); return ( <div> <div>Count: {state.count}</div> <button onClick={() => dispatch({ type: 'INCREMENT' })}>Increment</button> <button onClick={() => dispatch({ type: 'DECREMENT' })}>Decrement</button> </div> ); }; export default Counter;
리액트 디자인 패턴은 이러한 것에만 국한되지 않으며 이 외에도 구현 할 수 있는 여러 디자인 패턴이 있다.
그러니 내 프로젝트에 필요한 부분을 고민하고 그에 맞는 디자인패턴을 사용하면 관리에 훨씬 용이해질 것이다.
반응형'Front-end > React' 카테고리의 다른 글
npm publish 해보기 ( react-step-slider ) (0) 2025.01.07 리액트(React) 19버전 업데이트 전! 18버전 훑어보기! (0) 2024.05.21 TanStack Table v8 - Merge header cell (헤더 병합) (0) 2024.03.07 LocalStorage로 저장, 불러오기, 삭제 (JS, React) (2) 2023.02.05 리액트 파일 업로드, 파일 다운로드 File upload, download with javascript react (2) 2022.06.24