등 많은 장점들이 있는데요
오늘은 그 중에서도
컴포넌트 재사용성이 높음 을 중심적으로 살펴보고자 합니다!
우선 리액트는 컴포넌트 기반 구조를 가지고 있으며
Props와 State로 컴포넌트 간 데이터를 효율적으로 전달하고 관리할 수 있습니다.
재사용성
관심사 분리
응집도가 높은
-> 변경에 유연한 코드!
React 컴포넌트를 어떻게 더 잘 추상화 할 수 있느냐! 가 관건입니다.
소프트웨어 개발에서 주로 사용되는 용어로, 사용자 인터페이스(UI)를 가지지 않은 컴포넌트 또는 서비스를 이야기
여러 개의 작은 컴포넌트들이 각각의 역할을 분담하도록 하고 이를 조립해 하나의 큰 컴포넌트를 만드는 것이다.
이렇게 다양한 형태의 카드를 작성하기 위해선 어떻게 해야 할까요?
Props로 입력받는 경우
// prop 추가로 복잡해진 카드 컴포넌트
const CardItem = ({
imageUrl,
tagNumber,
name,
description,
rounded,
lineCnt,
className,
...
}: Props) => {
...
return (
<div className={className}>
{imageUrl && <img src={imageUrl} style={rounded ? { borderRadius: '50%' } : undefined}/>}
<div>
<span>{tagNumber}</span>
<span>{name}</span>
</div>
{description && <div>...</div>}
...
</div>
);
}
보통 이렇게 Props 로 입력받아서 처리해야 겠다~ 라고 생각합니다. 하지만 너무 복잡하다는 단점이 있죠
카드 형태의 아이템을 보여준다
라는 책임을 더 세분화하여 문제를 해결할 수 있습니다.
CardItem
의 책임을 RoundCardItem
,SquareCardItem
,MultiThumnailCard
등으로 적절히 나누면 복잡한 컴포넌트도, 불필요한 의존성도 없이 문제를 해결할 수 있게 됩니다.
책임을 적절하게 나눈 경우
const CardThumbnail = ({ url, size, rounded, className }: Props) => ...
const CardBody = ({ className, align, children }: Props) => ...
const CardTitle = ({ className, lineCnt, children }: Props) => ...
const CardIcon = () => ...
const RoundCardItem = (...) => {
return (
<div>
<CardThumbnail url={imageUrl} rounded />
<CardBody align="center">
<CardTitle>{tagNumber}</CardTitle>
<CardTitle lineCnt={2}>{name}</CardTitle>
</CardBody>
</div>
)
}
...
카드의 구성 요소들을 CardThumbnail, CardBody, CardTitle, CardIcon으로 나누어 추상화하였고, 이것들을 원하는 대로 조합하여 RoundCardItem, SquareCardItem등과 같은 다양한 카드 컴포넌트를 만들 수 있게 되었습니다. 책임을 세분화하여 다양한 카드 형태에 대응할 수 있는 유연한 구조가 되었습니다.
Context API
const InputContext = React.createContext({
id: "",
value: "",
type: "text",
onchange: () => {},
});
Context API를 이용해 컴포넌트 내부에서 공유할 데이터를 정의
부모 컴포넌트
export const InputWrapper = ({ id, value, type, onChange, children }) => {
const contextValue = { id, value, type, onChange };
return (
<InputContext.Provider value={contextValue}>
{children}
</InputContext.Provider>
);
};
Context API를 통해 데이터를 공유할 수 있도록 설정
자식 컴포넌트
const Input = ({ ...props }) => {
const { id, value, type, onChange } = useContext(InputContext);
return (
<input id={id} value={value} type={type} onChange={onChange} {...props} />
);
};
const Label = ({ children, ...props }) => {
const id = useContext(InputContext);
return (
<label id={id} {...props}>
{children}
</label>
);
};
Context API를 통해 데이터를 사용ㅇ함
부모 컴포넌트가 props로 받아서 Context에 저장시킨 데이터 가져옴
추가로 자신만의 props도 정의 가능
Props 설정
InputWrapper.Input = Input;
InputWrapper.Label = Label;
자식 컴포넌트를 부모 컴포넌트의 props로 등록
전체코드
import React, { useContext } from "react";
/** context api를 이용해서 컴포넌트 내부에서 공유할 데이터를 정의함 */
const InputContext = React.createContext({
id: "",
value: "",
type: "text",
onchange: () => {},
});
/** 부모 컴포넌트, context api를 통해 데이터를 공유할 수 있도록 설정 */
export const InputWrapper = ({ id, value, type, onChange, children }) => {
const contextValue = { id, value, type, onChange };
return (
<InputContext.Provider value={contextValue}>
{children}
</InputContext.Provider>
);
};
/** 자식 컴포넌트, context api를 통해 데이터 사용함
부모 컴포넌트가 props로 받아서 context에 저장시킨 기본 데이터도 가져오고, 추가로 자신만의 props를 받아서 사용할 수도 있음
*/
const Input = ({ ...props }) => {
const { id, value, type, onChange } = useContext(InputContext);
return (
<input id={id} value={value} type={type} onChange={onChange} {...props} />
);
};
const Label = ({ children, ...props }) => {
const id = useContext(InputContext);
return (
<label id={id} {...props}>
{children}
</label>
);
};
/** 자식 컴포넌트를 부모 컴포넌트의 props로 등록함 */
InputWrapper.Input = Input;
InputWrapper.Label = Label;
실제로 불러와서 사용하게 되면 아래와 같습니다.
App.js
/** 데이터 관리 */
const [name, setName] = useState("");
const handleChange = (event) => {
setName(event.target.value);
};
<InputWrapper id="name" value={name} type="text" onChange={handleChange}>
<InputWrapper.Label>Name</InputWrapper.Label>
<InputWrapper.Input />
</InputWrapper>
자식 요소에 어떤 것이 들어올지 모른다고 가정
부모 요소는 오직 데이터 로직만 가짐
자식 요소를 컴포넌트 통째로 받도록 구성하는 컴포넌트 입니다.
functionAsChildInput.js
import React, { useState } from "react";
const FunctionAsChildInput = ({ children }) => {
const [value, setValue] = useState("");
const handleChange = (event) => {
setValue(event.target.value);
};
return children({
value,
onchange: { handleChange },
});
};
export default FunctionAsChildInput;
children의 타입이 function임!
-> 자식의 어떤것이 들어올지 모르기 때문에
App.js
<FunctionAsChildInput>
{({ value, onChange }) => {
return (
<div className="input-container">
<label id="1">Name</label>
<input type={"text"} id="1" value={value} onChange={onChange} />
</div>
);
}}
</FunctionAsChildInput>
children에서는 매개변수로 받은 로직을 마크업에 따라서 마음대로 사용할 수 있게 된다.
위의 Function as Child에서 바디에 들어갈 로직들을 use** 커스텀 훅으로 정의해 사용처에서 실행시켜 사용하게 됩니다. 어느곳에서나 사용할 수 있게 됩니다!
useInput.js
-> 커스텀 훅에 해당
function useInput() {
const [value, setValue] = useState('');
const onChange = (event) => {
setValue(event.target.value);
};
return { value, onChange };
}
App.js
const {value : name, onChange : onChangeName} = useInput()
<label htmlFor='1'>Name</label>
<input type="text" id='1' value={name} onChange={onChangeName} />
value 속성은 name 변수에 할당, onChange 속성은 onChangeName 변수에 할당되게 됩니다.
(이미지출처: https://fe-developers.kakaoent.com/2022/221020-component-abstraction/)
특정 아티스트의 앨범을 테이블 형태로 보여주는 AlbumList 컴포넌트를 예시로 봅니다.
const AlbumList = ({ artistId }: Props) => {
const [albums, setAlbums] = useState([]);
const [dataLoaded, setDataLoaded] = useState(false);
const ref = useRef<HTMLDivElement>(null);
const mountedRef = useRef(true);
useEffect(() => {
const fetchAlbums = async () => {
const response = await fetch('/getAlbums');
const data = await response.json();
setAlbums(data);
setDataLoaded(true);
}
fetchAlbums();
}, []);
useEffect(() => {
// 뒤로 가기로 페이지를 이동하여 앨범들을 보여줄 경우 마지막 스크롤 위치를 계산하여 맞춰줍니다.
if (mountedRef.current && dataLoaded) {
const scrollTop = ...
ref.current.scrollTop = scrollTop;
mountedRef.current = false;
}
}, [dataLoaded]);
return (
<div ref={ref}>
{albums.map(album => (
<SquareCardItemList ... />
))}
</div>
);
}
AlbumList
의 책임은 특정 아티스트의 앨범 정보들을 보여준다
입니다. 이 책임 수행을 위해 아래처럼 특정 도메인과 얽혀있는 기능들이 함께 수행됩니다.
위 코드의 문제
이때 사용하는 것이 바로 커스텀 훅 입니다.
커스텀 훅
const useFetchAlbums = (artistId: string) => {
const [albums, setAlbums] = useState([]);
const [dataLoaded, setDataLoaded] = useState(false);
useEffect(() => {
const fetchAlbums = async () => {
const response = await fetch('/getAlbums');
const data = await response.json();
setAlbums(data);
setDataLoaded(true);
}
fetchAlbums();
}, []);
return [albums, dataLoaded]
}
const useLatestScrollTop = ({ ref, dataLoaded }: Props) => {
const mountedRef = useRef(true);
useEffect(() => {
// 뒤로 가기로 페이지를 이동하여 앨범들을 보여줄 경우 마지막 스크롤 위치를 계산하여 맞춰줍니다.
if (mountedRef.current && dataLoaded) {
const scrollTop = ...
ref.current.scrollTop = scrollTop;
mountedRef.current = false;
}
}, [dataLoaded]);
}
AlbumList 컴포넌트
const AlbumList = ({ artistId }: Props) => {
const [albums, dataLoaded] = useFetchAlbums(artistId);
const ref = useRef<HTMLDivElement>(null);
useLatestScrollTop({ ref, dataLoaded });
return (
<div ref={ref}>
{albums.map(album => (
<SquareCardItemList ... />
))}
</div>
);
}
useFetchAlbums
, useLatestScrollTop
두 가지 훅으로 각각의 기능을 추상화 했습니다.
장점
useFetchAlbums
훅을 사용하여 쉽게 대응 가능 (useLatestScrollTop
도 동일)[10분 테코톡] 호프의 프론트엔드에서 컴포넌트
React 컴포넌트와 추상화
React Headless 컴포넌트 개발 패턴