안녕하세요. RyuWoong 입니다.
이번에 말씀 드릴 이야기는! Component 모듈화하기입니다.
React App을 만들다보면, View와 Logic이 혼재되어 복잡해 보이는 문제가 발생합니다.
이를 기존에 있던 디자인 패턴이나, Hook 등을 이용하여 관심사를 분리하고, 더 가독성 좋고, 유지보수가 용이하게 할 수 있습니다! 많은 프론트 개발자들이 이러한 고민은 다 거쳐가시는것 같네요. 한번 알아볼까요?
아래 이야기는 여러 내용을 찾아보고 나름대로 이해하여 적용한 것으로 틀린게 있을 수 있습니다. 부족한게 있으면 댓글로 알려주세요!
발단.
React던 React Native던 규모가 커지면 커질수록 하나의 컴포넌트가 점점 비대해지고 분리하자니 Props Drilling Depth가 깊어지는 등의 고민도 나날이 커져갑니다. 한 파일에 200~300줄이 넘어가기 시작하면 읽기도 어려워지고, 문제가 발생하면 각 함수들은 무슨 역할을 하는지 읽어보며 찾아봐야하죠.
그래서 저는 비대해지는 컴포넌트를 아래와 같이 쪼갰습니다. 상태와 기능은 CutomHoook으로 View는 View대로 나눠 한 파일의 코드분량을 적게 유지하고 싶었습니다. 작은 부품들로 만든 후 하나로 합쳐 완성품을 만들고자 했습니다.
이러한 디자인을 위해 좋은 영감이 되는 글들과 영상들을 찾아보았습니다. 상태를 어떻게 관리할지, 자주 변경되는 컴포넌트들은 또 어떻게 관리할지, 컴포넌트를 만들때 어떠한 기준으로 만들고 재활용 할 것인가 등 React를 다루다 보면 자주 접할 고민에 대해 꽤나 많이 다루고 있었고 좋은 참고가 되었습니다. 해당 글들은 글 하단 참조란에 적어두겠습니다.
컴포넌트를 어떻게 만들 것인가?
만드는 방법은 정말 다양합니다. 데이터와 UI를 고려해서 어떻게 만들 것인지 잘 판단하는것이 중요하다고 생각합니다.
하나하나 자주 사용하는 방식을 한번 소개 해볼까 합니다.
일반적인 Component
가장 흔히 만들수 있는 방법은 Props로 데이터를 전달하는 방식이죠.
저는 React Native로 만들고 있는 App을 예제로 사용하여 이야기해볼까 합니다.
아래는 0000년 00월을 선택하는 날짜 선택 Dialog(Modal)입니다.
위 같이 MonthDatePicker Component는 visible, value, onSelect, onClose이라는 Props를 가지고 있습니다.
이 Component는 Picker가 나타나야하는지 판단하는 visible과 현재 선택된 날짜를 value로 받고 날짜를 선택하는 함수와 picker를 닫는 함수를 받습니다. 이렇게 설계한 이유는 날짜라는 특수성 때문에 내부에 리스트를 따로 만들고 있고, 변경하더라도 내부 스타일만 바뀐다고 생각했습니다.
Hoc (Higher-Order-Component)
고차 컴포넌트라 불리는 HOC입니다. Hook이 등장하기전에 유용하게 쓰던 패턴이고 저는 현재 쓰고 있는 부분이 없어서 Velopert님의 Hoc자료로 설명드리겠습니다. Component는 결국 함수이고 반복되는 것을 줄이는 것을 목표로 합니다.
import React, { Component } from 'react';
import axios from 'axios';
class Post extends Component {
state = {
data: null
}
async initialize() {
try {
const response = await axios.get('https://jsonplaceholder.typicode.com/posts/1');
this.setState({
data: response.data
});
} catch (e) {
console.log(e);
}
}
componentDidMount() {
this.initialize();
}
render() {
const { data } = this.state;
if (!data) return null;
return (
<div>
{ JSON.stringify(data) }
</div>
);
}
}
export default Post;
import React, { Component } from 'react';
import axios from 'axios';
class Comments extends Component {
state = {
data: null
}
async initialize() {
try {
const response = await axios.get('https://jsonplaceholder.typicode.com/comments?postId=1');
this.setState({
data: response.data
});
} catch (e) {
console.log(e);
}
}
componentDidMount() {
this.initialize();
}
render() {
const { data } = this.state;
if (!data) return null;
return (
<div>
{JSON.stringify(data)}
</div>
);
}
}
export default Comments;
위 Post와 아래 Comments에서 반복되는 부분이 보입니다.
데이터를 받아와서 뿌려주는 부분이죠. 이부분을 따로 빼고 return되는 것만 컴포넌트로 빼고 데이터를 받아올 수 있도록 props를 설정합니다. 아래와 같이 말이죠.
import React, { Component } from 'react';
import axios from 'axios';
const withRequest = (url) => (WrappedComponen) => {
return class extends Component {
state = {
data: null
}
async initialize() {
try {
const response = await axios.get(url);
this.setState({
data: response.data
});
} catch (e) {
console.log(e);
}
}
componentDidMount() {
this.initialize();
}
render() {
const { data } = this.state;
return (
<WrappedComponent {...this.props} data={data}/>
)
}
}
}
export default withRequest;
그럼 withRequest를 사용해볼까요? 아래와 같이 사용할 수 있습니다.
( withRequest를 보시면 () => () => 화살표 함수가 두 번 이어진 모습을 볼 수 있습니다. 이건 커링 이라는 기술입니다. )
import React, { Component } from 'react';
import withRequest from './withRequest';
class Post extends Component {
render() {
const { data } = this.props;
if (!data) return null;
return (
<div>
{ JSON.stringify(this.props.data) }
</div>
);
}
}
export default withRequest('https://jsonplaceholder.typicode.com/posts/1')(Post);
export 해주는 부분을 보시면 withRequest가 감싸 반복되는 요소를 처리해주고 Post로 정렬된 데이터를 Props로 주입해주면 Post는 View를 렌더해줍니다. 이 HOC는 Custom Hook으로 대체할 수 있고, Hook이 더 가독성 측면이 좋다고 생각이 드네요.
Compound Component
다음은 앞 전 포스팅 한 적 있는 Compound Component입니다.
Props Drilling을 피하고 싶거나, Component간 값을 공유 해야하는 경우나 Componet 유동성을 좀 더 크게 하고 싶은 경우 (상단에 헤더를 추가해주세요, 추가적인 기능을 중간에 끼우는 등) 많이 사용합니다. 많은 영상이나 발표에서 반복적으로 나오는 요소인 것 같습니다.
저는 Monthly라는 Component를 만들었습니다. Monthly는 Context API로 내부 Component에 상태를 전파함과 동시에 내부 Component를 감싸는 역할을 합니다. Monthly 내부에는 useDate라는 Custom Hook 만들어 사용하고 있습니다.
Custom Hook
잠시 useDate라는 CustomHook에 대해 설명을 드리려고 합니다. 서버까지 만들어 관리하면 운영비와 서버 설계까지 해야 하는 것은 너무 과하다고 판단했고 Severless 구성으로 만들기 위해 해당 앱의 서버는 Firebase로 구성되어 있습니다. DB는 Realtime Datebase로 No SQL을 사용하고 있습니다. 내가 원하는 날짜의 데이터 값을 DB에 요청해 받아오면 Total과 List에서 사용할 수 있게끔 데이터를 정리하는 로직과 데이터는 날짜를 조건으로 전달해주기 때문에 날짜 값과 날짜를 수정할 수 있는 값 또한 가지고 있습니다. 아래와 같이 값을 리턴해줍니다. (total은 분리해서 Total Component에 넣어도 될 것 같네요.)
// useDate.ts
function useDate() {
...
return {date, list, total, handleDate};
}
이어서...
Monthly는 Context API와 내부 Child로 사용할 수 있는 Component를 포함하고 있습니다. 이렇게 Monthly 내부의 값은 useContext를 사용하여 Monthly Context 값을 전파 받을 수 있습니다. (useContext를 반복해서 입력하기 귀찮으니 이것도 분리하여 useMonthly로 만들어 줍니다.)
// Monthly.tsx
function Monthly({children}: Props) {
const value = useDate();
return (
<MonthlyContext.Provider value={value}>{children}</MonthlyContext.Provider>
);
}
Monthly.List = MonthlyList;
Monthly.Total = MonthlyTotal;
export default Monthly;
MonthlyList와 MonthlyTotal은 Component내에 useMonthly만 호출하여 사용하고 View만 신경 쓸 수 있게 됩니다.
아래와 같이 데이터는 date, list만 사용하고 View에만 신경쓸 수 있죠!
위와 같은 방식을 이용하여, 컴포넌트를 어떻게 만들지, 어떻게 하면 재사용하기가 용이할지, 변경이 생길 때 잘 대쳐할 수 있을지를 고민하며 선택해서 사용할 수 있습니다. React를 사용하면서 뭔가 코드가 지저분하거나 비즈니스로직과 View를 나누고 싶은 생각이 드신다면 좋은 영감이 되셨으면 좋겠습니다.
Polymorphic Component
다음 다형성 컴포넌트라는 것이 있습니다.
간단하게 말하자면 좀 더 유동성 있는 컴포넌트를 만드는 것입니다.
예로 들어, button은 단순히 button뿐만 아니라 형태은 button이지만 태그와 기능은 link인 button을 만들고 싶을 수도 있습니다.
만약 Polymorphic Component를 만든다면, 우리는 JSX상에서 컴포넌트는 <Button /> 으로 보이지만, 실제 html 태그 상에서는 link로 표시 될 겁니다!
글이 너무 길어 진거 같아, 이 부분은 따로 포스팅할 예정입니다!
참조.
'Front-End > React' 카테고리의 다른 글
[React] Compound Component .10 (0) | 2023.02.15 |
---|---|
[React] Custom Hook .09 (0) | 2023.02.14 |
[React] Context API .08 (0) | 2023.02.05 |
[React] Hooks - useReducer .07 (0) | 2023.02.03 |
[React.js] Hooks - useRef .06 (0) | 2021.01.20 |
삽질의 기록과 일상을 남기는 블로그입니다. 주로 React Native를 다룹니다.
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!