리액트 hoc (번역)
✨리액트 hoc
리액트 공식문서를 읽어도 내용이 잘 이해되지 않아 Valentine Gatwiri 가 작성한 How to Use Higher-Order Components in React 를 번역한 글이다.
리액트 라이브러리의 강력한 기능 중 하나인 고차 컴포넌트(Higher-order components,HOCs)는 여러 컴포넌트 간에 컴포넌트 로직을 재사용할 수 있게 해줍니다.
리액트에서 고차 컴포넌트는 컴포넌트를 인자로 받고, 해당 컴포넌트를 감싼 새로운 컴포넌트를 반환하는 함수입니다.
HOCs를 사용하면 컴포넌트 코드를 수정하지 않고 컴포넌트에 기능을 추가할 수 있습니다. 예를 들어 인증 또는 라우팅 기능을 컴포넌트에 추가하거나 여러 컴포넌트에 특정 스타일이나 동작을 적용하는데 HOC를 사용할 수 있습니다.
HOCs는 추가 인자를 받을 수 있으므로 HOC의 동작을 사용자가 정의할 수 있습니다. 이를 통해 컴포넌트에 기능을 추가하고, 재사용이 가능하게 됩니다.
✅리액트에서 HOCs를 사용하는 이유
- 재사용성: HOCs를 사용하면 여러 컴포넌트 간에 컴포넌트 로직을 재사용할 수 있어 시간을 절약하고, 코드의 중복을 줄일 수 있다.
- 유연성: HOCs는 추가 인자를 받으므로 HOC의 동작을 사용자가 정의할 수 있다. 이는 컴포넌트에 기능을 추가하는 유연한 방법이 된다.
- 관심사 분리: HOCs는 코드에서 특정 기능을 캡슐화하여 관심사를 분리하는데 도움을 줄 수 있다. 코드의 가독성을 높여주고, 유지보수가 용이하게 만든다.
- 구성: HOCs를 사용하면 보다 복잡한 기능을 만들 수 있다. 재사용 가능한 조각들로부터 기능을 구축할 수 있다.
- HOCs는 인증, 오류처리, 로깅, 성능 추적 등과 같은 교차 관심사를 구현하는데 사용된다.
💡고차 컴포넌트(HOCs) 구조
리액트에서 고차 컴포넌트(HOCs)를 정의하기 위해 일반적으로 몇 가지 기본 단계를 따릅니다:
먼저, HOC 함수를 정의합니다. HOC 함수는 컴포넌트를 인자로 받고, 추가 기능이 적용된 새로운 컴포넌트를 반환하는 함수입니다.
1
2
3
const hoc = (WrappedComponent) => {
// ...
};
그런 다음 새로운 컴포넌트를 정의합니다. 이는 원래 컴포넌트를 감싸고 추가 기능을 추가하는 클래스 컴포넌트입니다.
1
2
3
4
5
6
class NewComponent extends React.Component {
// ...
render() {
// ...
}
}
다음으로, WrappedComponent props를 전달합니다. NewComponent의 render()메서드에서 추가된 props를 포함하여 모든 props를 WrappedComponent 전달합니다.
1
2
3
render() {
return <WrappedComponent {...this.props} additionalProp={additionalProp} />
}
마지막으로 NewComponent를 반환합니다. HOC 함수는 NewComponent를 반환하여 애플리케이션에서 사용할 수 있도록 합니다.
1
2
3
4
5
6
7
8
9
10
const hoc = (WrappedComponent) => {
class NewComponent extends React.Component {
// ...
render() {
// ...
}
}
return NewComponent;
};
✅리액트 코드에서 HOC를 사용하는 시점
1. 인증
사용자가 특정 경로에 접근하기 전에 인증이 필요한 애플리케이션을 가정해보겠습니다.
각 컴포넌트나 경로에서 인증 로직을 중복해서 작성하는 대신, 사용자가 인증되었는지 확인하고, 그렇지 않은 경우 로그인 페이지로 리다이렉션하는 withAuth
라는 HOC를 사용할 수 있습니다. 그리고 인증이 필요한 특정 컴포넌트나 경로를 HOC로 래핑하여 코드의 중복을 줄이고, 일관된 인증 동작을 강제화할 수 있습니다.
2. 로깅
특정 컴포넌트가 마운트되거나 업데이트될 때마다 데이터를 로그로 기록하고 싶다고 가정해보겠습니다. 각 컴포넌트에 로깅 로직을 추가하는 대신, 로깅 기능을 처리하는 withLogger
라는 HOC를 사용할 수 있습니다.
해당 컴포넌트를 withLogger
로 래핑함으로써 해당 컴포넌트 간에 일관된 로깅을 수행할 수 있습니다.
3. 스타일링, 테마
재사용 가능한 스타일과 테마를 가진 디자인 시스템이 있다고 가정해보겠습니다. withTheme
라는 HOC를 생성하여 해당 테마 관련 props를 제공할 수 있습니다.
이렇게 하면 래핑된 컴포넌트가 제공된 테마에 따라 적절한 스타일에 쉽게 접근하소 적용할 수 있습니다.
✅리액트에서 고차 컴포넌트를 사용하는 방법
리액트에서 HOC를 만들려면 컴포넌트를 인자로 받아들이고 해당 컴포넌트를 감싼 새로운 컴포넌트를 반환하는 함수를 정의합니다. 간단한 HOC 예는 다음과 같습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import React from "react";
import ReactDOM from "react-dom";
const withLoading = (WrappedComponent) => {
class WithLoading extends React.Component {
state = {
isLoading: true
};
componentDidMount() {
setTimeout(() => {
this.setState({ isLoading: false });
}, 2000);
}
render() {
return (
<WrappedComponent {...this.props} loading={this.state.isLoading} />
);
}
}
WithLoading.displayName = `withLoading(${
WrappedComponent.displayName || WrappedComponent.name
})`;
return WithLoading;
};
const MyComponent = ({ loading }) => (
<div>{loading ? <p>Loading...</p> : <p>Hello, world!</p>}</div>
);
const MyComponentWithLoading = withLoading(MyComponent);
ReactDOM.render(<MyComponentWithLoading />, document.getElementById("root"));
이 예제에서 withLoading
고차 컴포넌트는 WrappedComponent
를 인자로 받고, 새로운 컴포넌트 withLoading
를 반환하며 withLoading
는 WrappedComponent
에 loading prop을 추가합니다.
WithLoading
컴포넌트는 처음에 isLoading
의 상태를 true로 설정한 다음 2초 뒤에 false로 설정합니다. WrappedComponent
는 loading prop가 this.state.isLoading
으로 설정된 상태로 로딩됩니다.
MyComponet
는 loading...
메세지 또는 Hello, world!
메세지를 렌더링하는 함수형 컴포넌트입니다.
MyComponentWithLoading
컴포넌트는 MyComponent
를 withLoading
로 전달하여 생성됩니다. 마지막으로 MyComponentWithLoading
컴포넌트는 ReactDOM.render()
를 사용하여 렌더링됩니다.
✅ 로깅 HOC 예제
사용자가 특정 작업을 수행할 때 데이터를 기록하는 간단한 React 예제를 만들어 봅시다. “Todo List” 앱을 만들고, 사용자가 할일을 추가하거나 완료할 때마다 브라우저 콘솔에 이벤트를 기록하겠습니다.
시스템에 이미 Node.js와 npm이 설치되어 있다고 가정하고, 터미널창을 열어서 다음 명령을 실행합니다.
1
2
npx create-react-app hoc-example
cd hoc-example
src
폴더에 withLogger
HOC를 생성하고 withLogger.js
라는 새 파일을 만듭니다. 다음 코드를 파일에 복사합니다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// withLogger.js
import React, { useEffect } from "react";
const withLogger = (WrappedComponent) => {
const WithLogger = (props) => {
useEffect(() => {
// Log data on component mount
console.log(`Component ${WrappedComponent.name} mounted.`);
return () => {
// Log data on component unmount
console.log(`Component ${WrappedComponent.name} unmounted.`);
};
}, []);
useEffect(() => {
// Log data on component update
console.log(`Component ${WrappedComponent.name} updated.`);
});
return <WrappedComponent {...props} />;
};
WithLogger.displayName = `withLogger(${
WrappedComponent.displayName || WrappedComponent.name
})`;
return WithLogger;
};
export default withLogger;
위의 코드는 withLogger
라는 고차 컴포넌트(HOC)입니다. 이 경우, withLogger
HOC는 래핑하는 컴포넌트에 로깅 기능을 추가하고 있습니다.
“Todo List” 컴포넌트 작성하기
src
폴더에 TodoList.js
, TodoItem.js
, TodoForm
세 개의 파일을 만듭니다. 이 파일들을 Todo List에 포함시키겠습니다.
TodoList.js
에 다음 코드를 추가합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import React, { useState } from "react";
import TodoItem from "./TodoItem";
import withLogger from "./withLogger";
import TodoForm from "./TodoForm";
const TodoList = () => {
const [todos, setTodos] = useState([]);
const [newTodo, setNewTodo] = useState("");
const addTodo = () => {
if (newTodo.trim() !== "") {
setTodos((prevTodos) => [...prevTodos, newTodo]);
setNewTodo("");
}
};
const completeTodo = (index) => {
setTodos((prevTodos) => {
const updatedTodos = [...prevTodos];
updatedTodos.splice(index, 1);
return updatedTodos;
});
};
return (
<div>
<h1>할 일 목록</h1>
<TodoForm newTodo={newTodo} setNewTodo={setNewTodo} addTodo={addTodo} />
<ul>
{todos.map((todo, index) => (
<TodoItem
key={index}
todo={todo}
onComplete={() => completeTodo(index)}
/>
))}
</ul>
</div>
);
};
export default withLogger(TodoList);
위 코드에서는 TodoList
컴포넌트를 구현했고, 이를 withLogger
고차 컴포넌트를 사용하여 로깅 기능을 추가했습니다. TodoList
컴포넌트는 할 일 목록 상태를 관리하고, 할 일을 추가하고 완료하는 함수를 제공합니다.
TodoItem.js
에 다음 코드를 추가합니다.
1
2
3
4
5
6
7
8
9
10
11
12
import React from "react";
const TodoItem = ({ todo, onComplete }) => {
return (
<li>
{todo}
<button onClick={onComplete}>완료</button>
</li>
);
};
export default TodoItem;
위의 코드는 “Complete”버튼이 있는 하나의 할일 아이템을 렌더링하는 간단한 함수형 컴포넌트 입니다.
TodoItem
컴포넌트는 개별 할 일 항목을 표시하고, 사용자가 “Complete”버튼을 클릭하여 완료할 수 있도록 합니다. 할 일 목록 앱의 필수적이고 중요한 부분이며, TodoList
컴포넌트와 함께 사용하면 완벽하고 기능적인 할일 목록 환경을 제공할 수 있습니다.
TodoForm.js
에 다음 코드를 추가합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from "react";
const TodoForm = ({ newTodo, setNewTodo, addTodo }) => {
return (
<div>
<input
type="text"
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
placeholder="새 할 일을 입력하세요"
/>
<button onClick={addTodo}>할 일 추가</button>
</div>
);
};
export default TodoForm;
input 필드와 “Add Todo” 버튼을 렌더링해서, 사용자가 새로운 할 일을 추가할 수 있도록 하는 함수형 컴포넌트입니다.
이 TodoForm
컴포넌트를 사용하면 사용자는 input 필드에 새로운 할 일 아이템을 입력하고, “Add Todo”버튼을 클릭하면 TodoList
컴포넌트가 관리하는 목록에 추가할 수 있습니다.
MyComponent
를 사용하도록 앱 컴포넌트 업데이트하기
src
폴더에 있는 App.js
파일을 열고 다음 코드로 변경합니다.
1
2
3
4
5
6
7
8
9
10
11
12
import React from "react";
import TodoList from "./TodoList";
function App() {
return (
<div>
<TodoList />
</div>
);
}
export default App;
모든 파일을 저장하고, 터미널로 돌아갑니다. 여전히 프로젝트 root 폴더(hoc-example)에 있는지 확인합니다. 이제 다은 명령어를 입력하여 개발 서버를 실행시킵니다.
1
npm start
작업을 추가하거나 완료할 때마다 앱은 withLogger
HOC를 사용하여 브라우저 콘솔에 이벤트를 기록합니다. 예를 들어 콘솔에 다음과 같이 표시됩니다.
끝입니다! 이제 리액트 프로젝트에서 withLogger
이라는 고차 컴포넌트를 사용하는 예제를 구현했습니다. 전체 Github코드입니다.
✅결론
이 글에서는 리액트의 고차 컴포넌트(HOC)를 얘기했고, 재사용 가능하고, 유연한 컴포넌트를 구축할 때 고차 컴포넌트를 사용했을 때의 이점에 대해 얘기했습니다. 또한 고차 컴포넌트의 구조와 리액트에서 HOC를 빌드하는 방법에 대해 배웠습니다.