Skip to content

onef-1984/onef_front

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

๋ชฉ์ฐจ

ย  ย 

what is onef stands for

onef๋Š” "one-nine-eight-four"์˜ ๋‘๋ฌธ์ž๋กœ์„œ, ์ด ํ”„๋กœ์ ํŠธ๋ฅผ ์‹œ์ž‘ํ•  ์ˆ˜ ์žˆ๋„๋ก ์˜๊ฐ์„ ์ค€ ์กฐ์ง€ ์˜ค์›ฐ์˜ ์†Œ์„ค 1984๋ฅผ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค. onef๋Š” ๋…ํ›„๊ฐ์„ ์“ฐ๊ณ  ๊ณต์œ ํ•  ์ˆ˜ ์žˆ๋Š” ์„œ๋น„์Šค๋กœ์„œ, ๋…ํ›„๊ฐ์ด 1984์ž๋ฅผ ๋„˜์–ด์„œ๋Š” ์•ˆ ๋œ๋‹ค๋Š” ํŠน์ง•์„ ๊ฐ€์ง€๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ๋…ํ›„๊ฐ์„ ์“ด๋‹ค๋Š” ํ–‰์œ„ ์ž์ฒด์— ๋ถ€๋‹ด์„ ๋А๋ผ๋Š” ์‚ฌ๋žŒ๋“ค์—๊ฒŒ ๊ทธ ๋ถ€๋‹ด์„ ๋œ์–ด์ค„ ์ˆ˜ ์žˆ์ง€ ์•Š์„๊นŒ ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค.

ย 

๋ฐฐํฌ ์ฃผ์†Œ

์ฑ…์„ ์ฝ๊ณ  ์˜์›์„ ๊ธฐ๋กํ•˜๋‹ค - onef

ย 

๊ธฐ์ˆ  ์Šคํƒ

๊ฐœ๋ฐœ ์–ธ์–ด ๋ฐ ํ”„๋ก ํŠธ์—”๋“œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ :

ย 

์ƒํƒœ ๊ด€๋ฆฌ ๋„๊ตฌ :

ย 

ํ˜‘์—… ๋„๊ตฌ :

ย 

๊ธฐ๋Šฅ ๊ตฌํ˜„ ์†Œ๊ฐœ

github action๊ณผ AWS CodeDeploy๋ฅผ ์‚ฌ์šฉํ•œ CI/CD ๊ตฌ์ถ•

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๋ฅผ ์‚ฌ์šฉํ•œ ๋ฌดํ•œ ์Šคํฌ๋กค

์ƒํ’ˆ ๋ฆฌ์ŠคํŠธ๋‚˜ ๊ฒŒ์‹œ๊ธ€ ํ”ผ๋“œ์ฒ˜๋Ÿผ ๋ฐ์ดํ„ฐ๊ฐ€ ๋งŽ๊ณ  ๊ณ„์†ํ•ด์„œ ์ถ”๊ฐ€์ ์œผ๋กœ ๋กœ๋“œํ•ด์•ผ ํ•˜๋Š” ํŽ˜์ด์ง€๋ผ๋ฉด ๋ฐ˜๋“œ์‹œ๋ผ๊ณ  ํ•ด๋„ ๋  ์ •๋„๋กœ ๋ฌดํ•œ ์Šคํฌ๋กค ๊ธฐ๋Šฅ์„ ์ž์ฃผ ์‚ฌ์šฉํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ์ด ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ๋‹ค์–‘ํ•˜์ง€๋งŒ, ์ €๋Š” 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์„ ์‚ฌ์šฉํ•œ ์•Œ๋ฆผ ๊ธฐ๋Šฅ ๊ตฌํ˜„

๋‹ค๋ฅธ ์‚ฌ์šฉ์ž๊ฐ€ ๋‚ด๊ฐ€ ์ž‘์„ฑํ•œ ๋…ํ›„๊ฐ์— ์ข‹์•„์š”๋‚˜ ๋Œ“๊ธ€์„ ์ž‘์„ฑํ•œ ๊ฒฝ์šฐ, 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 };
}
default.webm

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages