๋ชฉ์ฐจ
- what is onef stands for
- ๋ฐฐํฌ ์ฃผ์
- ๊ธฐ์ ์คํ
- ๊ธฐ๋ฅ ๊ตฌํ ์๊ฐ
- github action๊ณผ AWS CodeDeploy๋ฅผ ์ฌ์ฉํ CI/CD ๊ตฌ์ถ
- ๊ณ ์ฐจ ์ปดํฌ๋ํธ ํจํด์ ์ฌ์ฉํ์ฌ ๋ฒํผ๊ณผ ๋งํฌ์ ์คํ์ผ ํต์ผ
- ์ผ๊ด์ ์ธ ๋ก์ง ์ฌ์ฉ์ ์ํ ์ ํธ๋ฆฌํฐ ์ปดํฌ๋ํธ ๋์
- ์ฌ๊ท ์ปดํฌ๋ํธ ํจํด์ ์ฌ์ฉํ์ฌ ๋๊ธ ๊ธฐ๋ฅ ๊ตฌํ
- IntersectionObserver API๋ฅผ ์ฌ์ฉํ ๋ฌดํ ์คํฌ๋กค
- ์ด๋ํฐ ํจํด์ ํ์ฉํ ๋ฐฑ์๋ ์์กด์ฑ ๊ฐ์
- ๋ ํฌ์งํ ๋ฆฌ ํจํด์ ํ์ฉํ ์ฟผ๋ฆฌ ํจ์ ๊ด๋ฆฌ
- websocket์ ์ฌ์ฉํ ์๋ฆผ ๊ธฐ๋ฅ ๊ตฌํ
ย ย
onef๋ "one-nine-eight-four"์ ๋๋ฌธ์๋ก์, ์ด ํ๋ก์ ํธ๋ฅผ ์์ํ ์ ์๋๋ก ์๊ฐ์ ์ค ์กฐ์ง ์ค์ฐ์ ์์ค 1984๋ฅผ ์๋ฏธํฉ๋๋ค. onef๋ ๋ ํ๊ฐ์ ์ฐ๊ณ ๊ณต์ ํ ์ ์๋ ์๋น์ค๋ก์, ๋ ํ๊ฐ์ด 1984์๋ฅผ ๋์ด์๋ ์ ๋๋ค๋ ํน์ง์ ๊ฐ์ง๊ณ ์์ต๋๋ค. ์ด๋ฅผ ํตํด ๋ ํ๊ฐ์ ์ด๋ค๋ ํ์ ์์ฒด์ ๋ถ๋ด์ ๋๋ผ๋ ์ฌ๋๋ค์๊ฒ ๊ทธ ๋ถ๋ด์ ๋์ด์ค ์ ์์ง ์์๊น ์๊ฐํ์ต๋๋ค.
ย
์ฑ ์ ์ฝ๊ณ ์์์ ๊ธฐ๋กํ๋ค - onef
ย
๊ฐ๋ฐ ์ธ์ด ๋ฐ ํ๋ก ํธ์๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ :
ย
์ํ ๊ด๋ฆฌ ๋๊ตฌ :
ย
ํ์ ๋๊ตฌ :
ย
main ๋ธ๋์น๋ก ์ฝ๋๊ฐ ๋จธ์ง๋๋ฉด ์๋์ผ๋ก github action์ด ๋์ํ์ฌ ์ด๋ฅผ S3์ AWS CodeDeploy๋ก ๋๊น๋๋ค. ๊ทธ๋ฌ๋ฉด AWS CodeDeploy๋ EC2 Instance ๋ด์ codedeploy-agent๋ฅผ ํตํด S3์ ์ฌ๋ผ๊ฐ ์ฝ๋ ๋ฌถ์์ Instance๋ก ๊ฐ์ ธ์ ์์ถ์ ํ๊ณ , ์ ํด์ง ๋ช ๋ น์ด๋ฅผ ์คํํฉ๋๋ค.
ci.webm
ย
ํ๋ก ํธ์๋๋ฅผ ์ฒ์ ์์ํ์ ๋๋ถํฐ ์ง๊ธ๊น์ง ์ ๊ฐ ๊ฐ์ง๊ณ ์๋ ๊ณ ๋ฏผ๊ฑฐ๋ฆฌ ์ค ํ๋๋ 'button ํ๊ทธ์ a ํ๊ทธ๊ฐ ์คํ์ผ์ด ๋์ผํ๋ค๋ฉด ์ด๋ฅผ ์ด๋ค ์์ผ๋ก ์ฒ๋ฆฌํ๋ ๊ฒ์ด ์ณ์๊ฐ'์ ๋๋ค. ์ด ๋ ๊ฐ์ ์ปดํฌ๋ํธ๋ ๋ฐ๋ props๋ ์กฐ๊ธ ๋ค๋ฅด๊ณ , ํด๋ฆญํ์ ๋์ ๋์๋ ๋ค๋ฅธ ํธ์ด์ง๋ง, ๊ฒฐ๊ตญ์ 'ํด๋ฆญํ ์ ์๋ ์์ญ'์ธ ๋งํผ UI ์ ์ผ๋ก ๋น์ทํ๊ฑฐ๋ ๋์ผํ ๋์์ธ์ ๊ฐ์ ธ๊ฐ๋ ๊ฒฝ์ฐ๊ฐ ์ข ์ข ์๊ธฐ ๋๋ฌธ์ ๋๋ค.
์ฒ์์๋ getClickable ์ด๋ผ๋ ํจ์๋ฅผ ์ฌ์ฉํด ์๋ก ๋ค๋ฅธ ์ปดํฌ๋ํธ์ ์คํ์ผ๋ง์ ์ฒ๋ฆฌํ์ง๋ง, Next.js๋ก ๋์ด์ค๋ฉด์ ์ค์ง ์คํ์ผ๋ง๋ง์ ์ํด Link ์ปดํฌ๋ํธ๋ฅผ ๊ฐ์ธ๋ ์๋ก์ด ์ปดํฌ๋ํธ๋ฅผ ๋ง๋๋ ๊ฒ์ด ์ ๊ป๋ฆ์นํ์ต๋๋ค. ๊ทธ๋์ ๊ณ ์ฐจ ์ปดํฌ๋ํธ ํจํด(HOC) ์ ์ฌ์ฉํ์ฌ button ํ๊ทธ์ Link ์ปดํฌ๋ํธ๋ฅผ ํ๋์ ๊ณ ์ฐจ ์ปดํฌ๋ํธ Clickable์์ ์ฒ๋ฆฌํ๋๋ก ํ์์ต๋๋ค.
type ClickableStyle = {
color?: "primary" | "white" | "like" | "kakao" | "borderless";
size?: "small" | "medium" | "large";
};
type ClickableProps<T extends ElementType> = ClickableStyle & ComponentPropsWithoutRef<T>;
export default function Clickable<T extends ElementType = "button">({
Component,
...props
}: {
Component?: T;
} & ClickableProps<T>) {
const Render = Component ?? "button";
const { color = "primary", size = "medium", className, ...restProps } = props;
const style = clsx(styles.root, styles[color], styles[size], className);
return <Render className={style} {...restProps} />;
}
Clickable.Container = ({ children }: { children: ReactNode }) => {
return <div className={styles.container}>{children}</div>;
};๊ธฐ๋ณธ์ ์ผ๋ก Clickable์ button ํ๊ทธ๋ก ๋๋๋ง๋์ง๋ง, ํ์ํ๋ค๋ฉด ๋ค๋ฅธ ํ๊ทธ ํน์ ์ปดํฌ๋ํธ๋ฅผ ์ฌ์ฉํ์ฌ ๋๋๋งํ ์ ์์ต๋๋ค.
// button ํ๊ทธ์ฒ๋ผ ์ฌ์ฉ
<Clickable type="submit" onClick={() => {}}>ํ์ธ</Clickable>
// Link ์ปดํฌ๋ํธ์ฒ๋ผ ์ฌ์ฉ
<Clickable component={Link} href="/">๋ฉ์ธ ํ๋ฉด์ผ๋ก ์ด๋</Clickable>ย
๋๊ท๋ชจ ํ ํ๋ก์ ํธ๋ฅผ ์งํํ๋ค๋ณด๋ฉด ์ปดํฌ๋ํธ๋ฅผ ์กฐ๊ฑด๋ถ๋ก ๋๋๋งํ๋ ๋ค์ํ ๋ฐฉ๋ฒ์ ๋ง์ฃผํ๊ฒ ๋ฉ๋๋ค. ๋๊ตฐ๊ฐ๋ not ์ฐ์ฐ์(||)๋ฅผ ์ฌ์ฉํ๊ณ , ๋ ๋๊ตฐ๊ฐ๋ ๋ ๋ณํฉ ์ฐ์ฐ์(??)๋ฅผ ์ฌ์ฉํ๊ณ , ๋ ๋๊ตฐ๊ฐ๋ ์ผํญ ์ฐ์ฐ์(?:)๋ฅผ ์ฌ์ฉํ๊ธฐ๋ ํฉ๋๋ค. Array.prototype.map๋ ๋น์ทํด์, ๊ฐ๋ฐ์๋ ์ ๋ง๋ค์ ์ฝ๋ ์คํ์ผ์ ๋ฐ๋ผ ์ด๋ฅผ ์์ฑํฉ๋๋ค.
์ ๋ ์ด๋ฌํ ํํธํ๋ฅผ ์กฐ๊ธฐ์ ๋ฐ๋ก์ก๊ณ ์ ๊ฐ๊ฐ์ ๋ก์ง์ ์ปดํฌ๋ํธ ๋ด๋ถ๋ก ์จ๊ธฐ๊ณ , ๊ฐ๋ฐ์๋ ์ปดํฌ๋ํธ์ ๊ท์น์ ๋ฐ๋ผ ์ฝ๋๋ฅผ ์์ฑํ๋ ์ ํธ๋ฆฌํฐ ์ปดํฌ๋ํธ Show์ Map ์ ๋ง๋ค์ด ์ฌ์ฉํ๊ณ ์์ต๋๋ค. ์ด๋ฅผ ํตํด ๊ฐ๋ฐ์๋ค์ ์กฐ๊ฑด๋ถ ๋ ๋๋ง๊ณผ ๋ฐ๋ณต ๋ ๋๋ง ์ ํน์ ํ ์ฝ๋ ์คํ์ผ์ ๋ฐ๋ฅด์ง ์์๋ ๋๊ณ , Show์ Map ์ปดํฌ๋ํธ๋ฅผ ํตํด ์ผ๊ด๋ ๋ฐฉ์์ผ๋ก UI๋ฅผ ๊ตฌ์ฑํ ์ ์์ต๋๋ค.
์ด๋ฌํ ์ ํธ๋ฆฌํฐ ์ปดํฌ๋ํธ๋ ์ฝ๋์ ๊ฐ๋ ์ฑ์ ๋์ด๊ณ , ๋๊ท๋ชจ ํ๋ก์ ํธ์์ ๋ฐ์ํ ์ ์๋ ์ฝ๋ ์คํ์ผ ํํธํ๋ฅผ ๋ฐฉ์งํ๋ ๋ฐ ํฐ ๋์์ ์ค๋๋ค.
export default function Show({
when,
children,
fallback = "",
}: {
when: boolean;
children: ReactNode;
fallback?: ReactNode;
}) {
return <>{when ? children : fallback}</>;
}export default function Map<T>({
each,
children,
fallback = "",
}: {
each: T[];
fallback?: ReactNode;
children: (item: T, index: number) => ReactNode;
}) {
return <>{each?.length !== 0 ? each.map(children) : fallback}</>;
}์๋๋ ์ ํธ๋ฆฌํฐ ์ปดํฌ๋ํธ Show๋ฅผ ์ฌ์ฉํ์ฌ ๋ค์ํ ์กฐ๊ฑด๋ถ ์ฐ์ฐ์๋ฅผ ๋์ฒดํ ์์์ ๋๋ค
// ์ผํญ ์ฐ์ฐ์๋ฅผ ๋์ฒด
<Show when={isLogin} fallback={<Header.SignLink />}>
<Header.ProfileImagePopover {...user} />
<Header.Notification {...user} />
</Show>
// and ์ฐ์ฐ์๋ฅผ ๋์ฒด
<Show when={toggle.BookSearchModal}>
<Dialog closeDialog={() => setToggle((prev) => ({ ...prev, BookSearchModal: false }))}>
<BookSearchModal />
</Dialog>
</Show>
// not ์ฐ์ฐ์๋ฅผ ๋์ฒด
<Show when={!!errorMessage}>
<span className={styles.errorMessage}>{errorMessage}</span>
</Show>ย
๋๊ธ ๊ธฐ๋ฅ์ ๊ตฌํํ ๋, ํนํ ๋ต๊ธ์ด ์ฌ๋ฌ ๋ฒ ์ค์ฒฉ๋๋ ๊ฒฝ์ฐ, ์ฌ๊ท ์ปดํฌ๋ํธ ํจํด์ ์ฌ์ฉํ๋ฉด ์ฝ๋๊ฐ ํจ์ฌ ๋ ๊น๋ํ๊ณ ์ ์ง๋ณด์๊ฐ ์ฌ์์ง๋๋ค. ์ฌ๊ท ์ปดํฌ๋ํธ๋ ์์ ์ ํธ์ถํ์ฌ ์ค์ฒฉ๋ ๊ตฌ์กฐ๋ฅผ ์์ฐ์ค๋ฝ๊ฒ ํํํ ์ ์๊ธฐ ๋๋ฌธ์, ๋๊ธ๊ณผ ๋ต๊ธ์ด ํธ๋ฆฌ ํํ๋ก ์ด์ด์ง๋ ๊ตฌ์กฐ์์ ์ ์ฉํฉ๋๋ค. ์๋ฅผ ๋ค์ด, ๊ฐ ๋๊ธ์ ์์ ์๋์ ์์ ๋๊ธ์ ๊ฐ์ง ์ ์๊ณ , ๊ทธ ์์ ๋๊ธ ๋ํ ์์ ๋๊ธ์ ๊ฐ์ง ์ ์์ต๋๋ค. ์ด๋, ์ฌ๊ท ์ปดํฌ๋ํธ๋ฅผ ์ฌ์ฉํ๋ฉด ๊ฐ ๋๊ธ์ ๋ณ๋์ ์ปดํฌ๋ํธ๋ก ๊ด๋ฆฌํ๋ฉด์ ์ค์ฒฉ๋ ๋๊ธ์ ์์ฐ์ค๋ฝ๊ฒ ๋ ๋๋งํ ์ ์์ต๋๋ค.
onef์์๋ depth ํ๋กํผํฐ๋ฅผ ์ฌ์ฉํด ์ฌ๊ท์ ์ผ๋ก ๋๊ธ์ ๊น์ด๋ฅผ ์ถ์ ํ๊ณ , ๊น์ด๊ฐ 5๋ณด๋ค ๊น์ด์ง๋ฉด ๋๋๊ธ์ ์์ฑํ ์ ์๋๋ก ํจ์ผ๋ก์จ ๋ฌดํํ ๊น์ด์ ๋๋๊ธ์ด ์์ฑ๋๋ ๊ฒ์ ์์น์ ์ผ๋ก ์ฐจ๋จํ๊ณ ์์ต๋๋ค.
Comment.Container = function CommentContainer({ id, depth }: { id: string; depth: number }) {
const { comments } = useQuery(new CommentQuery().getComments(id));
return (
<div className={styles.containerRoot}>
<Map each={comments}>
{(commentData) => {
return (
<div key={commentData.id}>
<Comment.Box key={commentData.id} depth={depth} commentData={commentData} />
<Comment.ReplyContainer commentData={commentData}>
<Comment.Container id={commentData.id} depth={depth + 1} />
</Comment.ReplyContainer>
</div>
);
}}
</Map>
</div>
);
};default.webm
ย
์ํ ๋ฆฌ์คํธ๋ ๊ฒ์๊ธ ํผ๋์ฒ๋ผ ๋ฐ์ดํฐ๊ฐ ๋ง๊ณ ๊ณ์ํด์ ์ถ๊ฐ์ ์ผ๋ก ๋ก๋ํด์ผ ํ๋ ํ์ด์ง๋ผ๋ฉด ๋ฐ๋์๋ผ๊ณ ํด๋ ๋ ์ ๋๋ก ๋ฌดํ ์คํฌ๋กค ๊ธฐ๋ฅ์ ์์ฃผ ์ฌ์ฉํ๊ฒ ๋ฉ๋๋ค. ์ด ๊ธฐ๋ฅ์ ๊ตฌํํ๋ ๋ฐฉ๋ฒ์ ๋ค์ํ์ง๋ง, ์ ๋ IntersectionObserver API๋ฅผ ์ปค์คํ ํ ์ผ๋ก ๊ฐ์ธ ํจ์จ์ ์ผ๋ก ์ฒ๋ฆฌํ ์ ์๋ ๋ฐฉ๋ฒ์ ์ ํํ์ต๋๋ค.
export const useInfiniteScroll = <T extends HTMLElement>(callback: Function) => {
// ๋ณด์ฌ์ง๊ณ ์๋์ง๋ฅผ ๋ํ๋ด๋ state
const [isVisible, setIsVisible] = useState(false);
const ref = useRef<T>(null);
useEffect(() => {
// ์ปดํฌ๋ํธ๊ฐ ๋ง์ดํธ ๋๋ฉด IntersectionObserver๋ฅผ ์์ฑ
const observer = new IntersectionObserver((entries) => {
const entry = entries[0];
// ๊ด์ฐฐ ๋์์ด ํ๋ฉด์ ๋ณด์ด๋ฉด isVisible์ true๋ก ์ค์
setIsVisible(entry.isIntersecting);
});
const currentRef = ref.current;
if (currentRef) {
// myRef๋ฅผ ๊ด์ฐฐ ๋์์ผ๋ก ์ค์
observer.observe(currentRef);
currentRef.style.minHeight = "1px"; // ๊ธฐ๋ณธ ๋์ด ์ค์ (optional)
}
// ์ปดํฌ๋ํธ๊ฐ ์ธ๋ง์ดํธ ๋๋ฉด observer๋ฅผ ํด์
return () => {
if (currentRef) {
observer.unobserve(currentRef); // ํน์ ๋์์ ์ธ์ต์ ๋ธํด์ผ ํจ
}
observer.disconnect(); // ์ต์ ๋ฒ ์์ฒด ํด์
};
}, []);
useEffect(() => {
if (isVisible) {
callback();
setIsVisible(false);
}
}, [isVisible, callback, setIsVisible]);
return ref;
};์ปดํฌ๋ํธ์์๋ useInfiniteScroll๋ฅผ ํธ์ถํ์ฌ ์๋์ ๊ฐ์ด ์ฌ์ฉํ ์ ์์ต๋๋ค.
const { fetchNextPage, pages } = useInfiniteBookListAdaptor(searchKeyword);
const ref = useInfiniteScroll<HTMLDivElement>(fetchNextPage);
return (
<div className={clsx(styles.bookSearchResult, styles.bookSearchSize)}>
<Map each={pages}>
{(book) => (
<Card key={book.isbn13} item={book} onClick={() => setBook(book)} cardBox={<CardResultBox {...book} />} />
)}
</Map>
<div ref={ref} />
</div>
);default.webm
ย
๋ง์ฝ ์ด๋ ํ ์ด์ ๋ก ๋ฐฑ์๋์์ ์๋ตํ๋ JSON ๋ฐ์ดํฐ์ ํ์์ด ๋ฌ๋ผ์ง๊ฒ ๋๋ค๋ฉด ์ด๋ป๊ฒ ๋ ๊น์. ์๋์ ์์์ ๊ฐ์ด ์ปดํฌ๋ํธ๊ฐ ์๋ฒ ์ํ๋ฅผ ์ง์ ์ ์ผ๋ก ๋ฐ๋ผ๋ณด๊ณ ์๋ ๊ฒฝ์ฐ๋ผ๋ฉด, ๋ชจ๋ ์ปดํฌ๋ํธ๋ฅผ ์์ ํด์ฃผ์ด์ผ ํ ๊ฒ๋๋ค. ์๋น์ค์ ์ด๊ธฐ ๋จ๊ณ๋ผ ๋ฐฑ์๋ ์คํ์ด ์์ฃผ ๋ฐ๋๋ ๊ฒฝ์ฐ๋ผ๋ฉด ์๋ก์ด ์ฝ๋๋ฅผ ์ง๋ ์๊ฐ๋ณด๋ค ์ปดํฌ๋ํธ ์์ ํ๋ ๋ฐ ์๊ฐ์ ๋ ์ฐ๊ฒ ๋ ์ง๋ ๋ชจ๋ฆ ๋๋ค.
const bookQuery = new BookQuery();
const { data, fetchNextPage } = useInfiniteQuery(bookQuery.getBookList(searchKeyword));
const ref = useInfiniteScroll<HTMLDivElement>(fetchNextPage);
const pages = data?.pages ?? []
return (
<div className={clsx(styles.bookSearchResult, styles.bookSearchSize)}>
<Map each={pages}>
{({ bookList }) => {
return (
<Map each={bookList.items}>
{(book) => (
<Card
key={book.isbn13}
item={book}
onClick={() => setBook(book)}
cardBox={<CardResultBox {...book} />}
/>
)}
</Map>
);
}}
</Map>
<div ref={myRef} />
</div>
);
}๋ฐ๋ผ์ ์ ๋ ์ปดํฌ๋ํธ๊ฐ ๋ฐฑ์๋๋ฅผ ์ง์ ๋ฐ๋ผ๋ณด๋ ๊ฒ์ด ์๋๋ผ, ์๋ฒ ์๋ต์ ๋งค๊ฐํด์ค ์ปค์คํ ์ด๋ํฐ ํ ์ ์์กดํ๋๋ก ํ์์ต๋๋ค. ๋๋ถ์ ์๋ฒ ์๋ต ํ์์ด ๋ณ๊ฒฝ๋ ๋๋ง๋ค ๋ชจ๋ ์ปดํฌ๋ํธ๋ฅผ ์์ ํ ํ์ ์์ด, ์ด๋ํฐ ํ ์ ์์ ํ์ฌ ์ด๋ฅผ ์ ์ ํ ์ฒ๋ฆฌํ ์ ์์ต๋๋ค.
export const useInfiniteBookListAdaptor = (searchKeyword: string) => {
const bookQuery = new BookQuery();
const { data, fetchNextPage } = useInfiniteQuery(bookQuery.getBookList(searchKeyword));
const pages = data?.pages.map((page) => page.bookList.items).flatMap((items) => items) ?? [];
return {
fetchNextPage,
pages,
};
};function BookListSearchResult({
searchKeyword,
setBook,
}: {
searchKeyword: string;
setBook: Dispatch<SetStateAction<Item>>;
}) {
const { fetchNextPage, pages } = useInfiniteBookListAdaptor(searchKeyword);
const ref = useInfiniteScroll<HTMLDivElement>(fetchNextPage);
return (
<div className={clsx(styles.bookSearchResult, styles.bookSearchSize)}>
<Map each={pages}>
{(book) => (
<Card key={book.isbn13} item={book} onClick={() => setBook(book)} cardBox={<CardResultBox {...book} />} />
)}
</Map>
<div ref={ref} />
</div>
);
}ย
์ด๋ํฐ ํจํด์ ์ฌ์ฉํ๋ค ๋ณด๋ฉด ๋ฐ์ดํฐ ์์ฒญ์ ์ฌ๋ฌ ํ ์์ ๊ด๋ฆฌํ๊ฒ ๋์ด ๋ฐ์ดํฐ ์์ฒญ ๋ก์ง์ด ๋ถ์ฐ๋๊ณ , ๋๋ฌธ์ ์ ์ง๋ณด์๊ฐ ์ด๋ ค์์ง ์ ์์ต๋๋ค. ์ด๋ฐ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ ํฌ์งํ ๋ฆฌ ํจํด์ ํ์ฉํ๋ฉด, ๋ฐ์ดํฐ ์์ฒญ ๋ก์ง์ ํ ๊ณณ์์ ๊ด๋ฆฌํ ์ ์์ต๋๋ค. ๋ ํฌ์งํ ๋ฆฌ ํจํด์ ๋ฐ์ดํฐ ์ฒ๋ฆฌ์ ๊ด๋ จ๋ ๋ก์ง์ ํ๋์ ์ถ์ํ๋ ๊ณ์ธต์ผ๋ก ๋ถ๋ฆฌํ์ฌ, ๋ฆฌ์กํธ ์ปดํฌ๋ํธ์ ๋น์ฆ๋์ค ๋ก์ง์ด ๋ฐ์ดํฐ ์์ค์ ์์กดํ์ง ์๋๋ก ๋ง๋ญ๋๋ค.
export class QueryFn {
queryFn<T>(url: string) {
return () =>
fetcher<T>({
method: "get",
url,
});
}
infiniteQueryFn<T>(url: string) {
return ({ pageParam }: { pageParam: number }) =>
fetcher<T>({
method: "get",
url: `${url}&skip=${pageParam}`,
});
}
}export class BookQuery extends QueryFn {
constructor() {
super();
}
queryKey = ["book"];
getBook(isbn13: string) {
return {
queryKey: [...this.queryKey, isbn13, "getBook"],
queryFn: () =>
this.fetcher<GetBookQuery>(`/book/${isbn13}`),
enabled: !!isbn13,
};
}
}ํนํ ๋ฆฌ์กํธ ์ฟผ๋ฆฌ๋ฅผ ์ฌ์ฉํ ๊ฒฝ์ฐ ์ฟผ๋ฆฌ ํค ๊ด๋ฆฌ์ ์์ด ์ค์ํ ์ ์ ์ผ๊ด์ฑ๊ณผ ์ฌ์ฌ์ฉ์ฑ์ ๋๋ค. ์ฌ๋ฌ ์ปดํฌ๋ํธ์์ ๋์ผํ ๋ฐ์ดํฐ๋ ๋น์ทํ ๋ฐ์ดํฐ๋ฅผ ์์ฒญํ ๋, ๊ฐ ์์ฒญ์ ๋ํด ์ผ๊ด๋ ์ฟผ๋ฆฌ ํค๋ฅผ ์ฌ์ฉํด์ผ ์บ์ฑ ๋ฐ ๋ฆฌํจ์นญ ์ ๋ต์ ํจ์จ์ ์ผ๋ก ๊ด๋ฆฌํ ์ ์์ต๋๋ค. ๋ ํฌ์งํ ๋ฆฌ ํจํด์ ์ฌ์ฉํ๋ฉด ์ด๋ฌํ ์ฟผ๋ฆฌ ํค์ ์ฟผ๋ฆฌ ํจ์๋ค์ ํ ๊ณณ์์ ๊ด๋ฆฌํ ์ ์์ด, ์ฟผ๋ฆฌ ํค์ ์ค๋ณต์ ๋ฐฉ์งํ๊ณ ์ฝ๋์ ์ฌ์ฌ์ฉ์ฑ์ ๋์ด๋ ๋ฐ ์ ๋ฆฌํฉ๋๋ค.
์๋ฅผ ๋ค์ด, BookQuery ํด๋์ค์์ getBook ๋ฉ์๋๋ ISBN ๋ฒํธ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์ฑ ์ ๋ณด๋ฅผ ์์ฒญํ๋ ์ฟผ๋ฆฌ๋ฅผ ์ ์ํ๊ณ ์์ต๋๋ค. ์ด๋ฅผ ํตํด ์ฟผ๋ฆฌ ํค์ ์ฟผ๋ฆฌ ํจ์๊ฐ ๋ช ํํ๊ฒ ๊ด๋ฆฌ๋๊ณ , ํ์ํ ๊ณณ์์ ์ฝ๊ฒ ํธ์ถํ ์ ์์ต๋๋ค. ์ด ๋ฐฉ์์ผ๋ก ์ฌ๋ฌ API ์์ฒญ์ ์ฒ๋ฆฌํ ๋, ์ฟผ๋ฆฌ ํค์ ์ฟผ๋ฆฌ ํจ์์ ์ฌ์ฌ์ฉ์ฑ์ ๋์ด๊ณ , ๊ฐ ์์ฒญ์ ์ธ๋ถ ๊ตฌํ์ ๋ ํฌ์งํ ๋ฆฌ ํด๋์ค์์ ๊ด๋ฆฌํจ์ผ๋ก์จ ๋ฆฌ์กํธ ์ปดํฌ๋ํธ๊ฐ ๋ณต์กํด์ง๋ ๊ฒ์ ๋ฐฉ์งํ ์ ์์ต๋๋ค.
onef์ ์ ์ฒด์ ์ธ ๋ฐ์ดํฐ ํ์นญ ์ ๋ต์ ์๋์ ๊ฐ์ต๋๋ค.
ย
๋ค๋ฅธ ์ฌ์ฉ์๊ฐ ๋ด๊ฐ ์์ฑํ ๋ ํ๊ฐ์ ์ข์์๋ ๋๊ธ์ ์์ฑํ ๊ฒฝ์ฐ, websocket์ ํตํด ์ฆ์ ์๋ฆผ์ ๋ฐ์ ์ ์์ต๋๋ค. ์๋ ์ฝ๋์์ useSocket ํ ์ WebSocket ์๋ฒ์์ ์ฐ๊ฒฐ์ ๊ด๋ฆฌํ๋ฉฐ, ์ง์ ๋ ์ด๋ฒคํธ๊ฐ ๋ฐ์ํ ๋๋ง๋ค callback ํจ์๋ฅผ ํธ์ถํ์ฌ ํ์ํ ์๋ฆผ์ ์ฒ๋ฆฌํฉ๋๋ค. ์ด๋ ๊ฒ ์ค์๊ฐ์ผ๋ก ์๋ฆผ์ ๋ฐ์ ์ ์๋ ํ๊ฒฝ์ ๊ตฌํํจ์ผ๋ก์จ, ์ฌ์ฉ์์๊ฒ ์ฆ๊ฐ์ ์ธ ํผ๋๋ฐฑ์ ์ ๊ณตํ ์ ์์ต๋๋ค.
export const useSocket = (userId: string, event: string, callback: (data: any) => void) => {
useEffect(() => {
socket.emit("userConnect", { userId });
socket.on(event, callback);
return () => {
socket.off(event, callback);
};
}, [userId, event, callback]);
};export default function useNotification(userId: string) {
const queryClient = useQueryClient();
const notificationQuery = new NotificationQuery();
const { data } = useQuery(notificationQuery.getNotifications(userId));
useSocket(userId, "notification", () => {
queryClient.invalidateQueries({ queryKey: ["notification"], refetchType: "all" });
});
const { newData, isNew } = formatData(data ?? []);
return { isNew, data: newData };
}