๋ฐ˜์‘ํ˜•

์บ๋กœ์…€ ๊ตฌ์กฐ


๋งŽ์€ ์›น์‚ฌ์ดํŠธ์—์„œ ์—ฌ๋Ÿฌ ์ด๋ฏธ์ง€๋ฅผ ์Šฌ๋ผ์ด๋“œ ํ˜•์‹์œผ๋กœ ๋ณด์—ฌ์ฃผ๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•˜๋Š” ์บ๋กœ์…€ ๋ทฐ์–ด๋Š” ์ƒ๊ฐ๋ณด๋‹ค ๊ฐ„๋‹จํ•˜๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค. ์บ๋กœ์…€ ๋ทฐ์–ด์˜ DOM ๊ตฌ์กฐ๋Š” ๋Œ€๋žต ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

<div> <!-- ์บ๋กœ์…€ ์•„์ดํ…œ Wrapper -->
  <div> <!-- ์บ๋กœ์…€ ์•„์ดํ…œ Parent -->
    <div /> <!-- ์บ๋กœ์…€ ์•„์ดํ…œ A -->
    <div /> <!-- ์บ๋กœ์…€ ์•„์ดํ…œ B -->
    <div /> <!-- ์บ๋กœ์…€ ์•„์ดํ…œ C -->
  </div>
</div>

 

  1. ์บ๋กœ์…€ ์•„์ดํ…œ Wrapper : ๋„˜์นจ ์˜์—ญ ์ˆจ๊น€ ์ฒ˜๋ฆฌ overflow: hidden; width: 100%; height: 100%;
  2. ์บ๋กœ์…€ ์•„์ดํ…œ Parent : ์—ฌ๋Ÿฌ๊ฐœ์˜ ์บ๋กœ์…€ ์•„์ดํ…œ์„ ๊ฐ์‹ธ๋Š” ๋ถ€๋ชจ โšก๏ธ
    • ์บ๋กœ์…€ ์•„์ดํ…œ๋“ค์˜ ์ˆ˜ํ‰ ์Œ“์ž„์„ ์œ„ํ•ด Flexbox ๋ ˆ์ด์•„์›ƒ ์ ์šฉ
    • ์Šคํฌ๋กค๋ฐ” ์ˆจ๊น€ ์ฒ˜๋ฆฌ
    • ์ „ํ™˜ ํšจ๊ณผ(transition)
    • ๋‹ค์Œ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅผ ๋•Œ๋งˆ๋‹ค ์ขŒ์ธก์œผ๋กœ ์ด๋™ transform: translateX(-100%|-200%|...)
  3. ์บ๋กœ์…€ ์•„์ดํ…œ : 1๊ฐœ ์•„์ดํ…œ๋งŒ ๋ณด์ด๋„๋ก ์ฒ˜๋ฆฌ width: 100%; flex-grow: 1; flex-shrink: 0;

 

๊ทธ๋ฆผ์œผ๋กœ ํ‘œํ˜„ํ•˜๋ฉด ์•„๋ž˜ ๊ฐ™์€ ๊ตฌ์กฐ๊ฐ€ ๋œ๋‹ค. ๊ฐ ์บ๋กœ์…€ ์•„์ดํ…œ์˜ ํญ์ด 100%(Parent ์š”์†Œ ๋„ˆ๋น„)์ด๋ฏ€๋กœ ํ™”๋ฉด์—” 1๊ฐœ ์•„์ดํ…œ๋งŒ ๋ณด์ธ๋‹ค(Item A). Parent ์š”์†Œ์˜ translateX ๊ฐ’์„ -100% ๋กœ ์„ค์ •ํ•˜๋ฉด ์ž์‹  ๋„ˆ๋น„ ๋งŒํผ ์ขŒ์ธก์œผ๋กœ ์ด๋™ํ•ด์„œ ๋‘๋ฒˆ์งธ ์•„์ดํ…œ(Item B)์ด ํ™”๋ฉด์— ๋ณด์ธ๋‹ค. ์—ฌ๊ธฐ์— transition: transform 250ms linear; ๊ฐ™์€ ์†์„ฑ์„ ์ฃผ๋ฉด ์บ๋กœ์…€ ์•„์ดํ…œ์ด ์›€์ง์ด๋Š” ๋“ฏํ•œ ํšจ๊ณผ๋ฅผ ์ค„ ์ˆ˜ ์žˆ๋‹ค.

 

 

  • transform: translateX(-0%) : Item A ํ™”๋ฉด์— ํ‘œ์‹œ
  • transform: translateX(-100%) : Item B ํ™”๋ฉด์— ํ‘œ์‹œ
  • transform: translateX(-200%) : Item C ํ™”๋ฉด์— ํ‘œ์‹œ

 

React์—์„œ ๊ตฌํ˜„


React์—์„  ํ˜„์žฌ ์•„์ดํ…œ์— ๋Œ€ํ•œ Index๋ฅผ ์ƒํƒœ๋กœ ๋‘๊ณ (currentIndex) 100์„ ๊ณฑํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค. Index ์ƒํƒœ๋Š” Next ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด 1์”ฉ ๋Š˜์–ด๋‚˜๊ณ , Prev ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด 1์”ฉ ๊ฐ์†Œํ•œ๋‹ค.

const [currentIndex, setCurrentIndex] = useState(0);
// Item A : Index 0 -> translateX(-0%);
// Item B : Index 1 -> translateX(-100%);
// Item C : Index 2 -> translateX(-200%);
// ...

return (
  <div // ์บ๋กœ์…€ ์•„์ดํ…œ Parent
    className="flex no-scrollbar transition-all ease-linear"
    style={{
      transform: `translateX(-${currentIndex * 100}%)`,
      transitionDuration: `${speed}ms`,
    }}
  >
    {children} {/* ์บ๋กœ์…€ ์•„์ดํ…œ */}
  </div>
);

 

์•„์ดํ…œ width์™€ translateX ๊ด€๊ณ„ โšก๏ธ


ํ™”๋ฉด์— ํ‘œ์‹œ๋˜๋Š” ์•„์ดํ…œ ๊ฐœ์ˆ˜๋ฅผ ๋ณ€๊ฒฝํ•˜๊ณ  ์‹ถ์œผ๋ฉด ์•„์ดํ…œ์˜ ํญ์„ ์ˆ˜์ •ํ•œ๋‹ค. ์Šฌ๋ผ์ด๋“œ ํ•  ๋•Œ์˜ ์•„์ดํ…œ ๊ฐœ์ˆ˜๋ฅผ ๋ณ€๊ฒฝํ•˜๊ณ  ์‹ถ์œผ๋ฉด translateX ๋ถ€๋ถ„์—์„œ ํ˜„์žฌ ์ธ๋ฑ์Šค ๋’ค์— ๊ณฑํ•˜๋Š” ์ˆซ์ž๋ฅผ ์ˆ˜์ •ํ•˜๋ฉด ๋œ๋‹ค.

 

์•„์ดํ…œ ํญ์„ 50%๋กœ ์„ค์ •ํ•˜๊ณ  translateX์— 100์„ ๊ณฑํ–ˆ๋‹ค๋ฉด 2๊ฐœ ์•„์ดํ…œ์ด ์Šฌ๋ผ์ด๋“œ๋œ๋‹ค. ์ด์ฒ˜๋Ÿผ ์•„์ดํ…œ ํญ๊ณผ translateX๋ฅผ ์กฐ์ •ํ•ด์„œ ํ”„๋กœ์ ํŠธ์—์„œ ์š”๊ตฌํ•˜๋Š” ๋ฐฉ์‹์— ๋งž์ถฐ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.

 

  1. ์•„์ดํ…œ width 100% | translateX(-${currentIndex * 100}%)
    • ํ™”๋ฉด ํ‘œ์‹œ ์•„์ดํ…œ ๊ฐœ์ˆ˜ : 1๊ฐœ
    • ์Šฌ๋ผ์ด๋“œ ์•„์ดํ…œ ๊ฐœ์ˆ˜ : 1๊ฐœ
  2. ์•„์ดํ…œ width 50% | translateX(-${currentIndex * 50}%)
    • ํ™”๋ฉด ํ‘œ์‹œ ์•„์ดํ…œ ๊ฐœ์ˆ˜ : 2๊ฐœ
    • ์Šฌ๋ผ์ด๋“œ ์•„์ดํ…œ ๊ฐœ์ˆ˜ : 1๊ฐœ
  3. ์•„์ดํ…œ width 50% | translateX(-${currentIndex * 100}%)
    • ํ™”๋ฉด ํ‘œ์‹œ ์•„์ดํ…œ ๊ฐœ์ˆ˜ : 2๊ฐœ
    • ์Šฌ๋ผ์ด๋“œ ์•„์ดํ…œ ๊ฐœ์ˆ˜ : 2๊ฐœ

 

๋งˆ์šฐ์Šค / ํ„ฐ์น˜ ๋“œ๋ž˜๊ทธ ์Šฌ๋ผ์ด๋“œ


๋”๋ณด๊ธฐ
export type CoordinateKeys = 'clientX' | 'clientY' | 'pageX' | 'pageY';
export type SwipeEvent<T = HTMLDivElement> = TouchEvent<T> | MouseEvent<T>;

const getCoordinates = (
  event: SwipeEvent,
): { [key in CoordinateKeys]: number } => {
  const swipeEvent = 'touches' in event ? event.touches[0] : event;
  const { clientX, clientY, pageX, pageY } = swipeEvent;

  return { clientX, clientY, pageX, pageY };
};

๋งˆ์šฐ์Šค / ํ„ฐ์น˜ ๋“œ๋ž˜๊ทธ๋กœ ์บ๋กœ์…€ ์•„์ดํ…œ์„ ์ขŒ / ์šฐ๋กœ ์›€์ง์ด๊ณ ,
์ผ์ • ๊ตฌ๊ฐ„ ์ด์ƒ ์ด๋™ํ•œ ํ›„ ๋“œ๋ž˜๊ทธ๋ฅผ ์ข…๋ฃŒ ํ–ˆ์„ ๋•Œ ์ด์ „ / ๋‹ค์Œ ์•„์ดํ…œ์œผ๋กœ ์ด๋™ํ•œ๋‹ค

 

์ƒํƒœ ๊ตฌ๋ถ„

  1. ๋ฆฌ๋ Œ๋”๋ง์— ์˜ํ–ฅ์„ ์ฃผ๋Š” ์ƒํƒœ — useState
    1. (number) diff : ์Šค์™€์ดํ”„ ์‹œ์ž‘ clientX - ์Šค์™€์ดํ”„ ์ข…๋ฃŒ clientX ๊ฒฐ๊ณผ๊ฐ’
    2. (number) currentIndex : ํ˜„์žฌ ํ™”๋ฉด์— ํ‘œ์‹œํ•˜๊ณ  ์žˆ๋Š” ์•„์ดํ…œ ์ธ๋ฑ์Šค
  2. ๋ฆฌ๋ Œ๋”๋ง์— ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š๋Š” ์ƒํƒœ — useRef ๋“ฑ์œผ๋กœ ๊ด€๋ฆฌ
    1. (null | number) swipeStartPos : ์Šค์™€์ดํ”„ ์‹œ์ž‘ clientX
    2. (boolean) isActiveTransition : ์ „ํ™˜ ํšจ๊ณผ ON / OFF ์—ฌ๋ถ€
    3. (boolean) isOnTransitionEvent : ์ „ํ™˜ ์ด๋ฒคํŠธ ์ง„ํ–‰์ค‘ ์—ฌ๋ถ€

 

์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ ํ†บ์•„๋ณด๊ธฐ

๐Ÿ’ก ์ฐธ๊ณ  ์‚ฌํ•ญ

  • clientX : ๋ธŒ๋ผ์šฐ์ € ํ™”๋ฉด ๊ธฐ์ค€ ๊ฐ€์žฅ ์™ผ์ชฝ์—์„œ ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•œ ์ง€์ ๊นŒ์ง€ ๊ฑฐ๋ฆฌ
  • ์ „ํ™˜ ์ด๋ฒคํŠธ (์ „ํ™˜ ์ด๋ฒคํŠธ ์‹œ์ž‘ / ์ข…๋ฃŒ์‹œ isOnTransitionEvent ๋‚ด๋ถ€ ์ƒํƒœ ๋ณ€๊ฒฝ)
    • ์ „ํ™˜ ํšจ๊ณผ ์‹œ์ž‘ ์ด๋ฒคํŠธ : transitionstart (React ์–ดํŠธ๋ฆฌ๋ทฐํŠธ : ์—†์Œ)
    • ์ „ํ™˜ ํšจ๊ณผ ์ข…๋ฃŒ ์ด๋ฒคํŠธ : transitionend (React ์–ดํŠธ๋ฆฌ๋ทฐํŠธ : onTransitionEnd)

 

์Šค์™€์ดํ”„ ์‹œ์ž‘ ํ•ธ๋“ค๋Ÿฌ

  • ์ด๋ฒคํŠธ ์œ ํ˜• : onTouchStart / onMouseDown
  • ์‹คํ–‰ ์กฐ๊ฑด : ์ „ํ™˜ ์ด๋ฒคํŠธ ์ง„ํ–‰์ค‘์ด ์•„๋‹˜ — isOnTransitionEvent === false
  • ํ•ธ๋“ค๋Ÿฌ ์•ก์…˜
    1. ์ด๋ฒคํŠธ ๊ฐ์ฒด์—์„œ ํ˜„์žฌ clientX ์ขŒํ‘œ ํš๋“
    2. swipeStartPos ์ƒํƒœ์— clientX ์ขŒํ‘œ ์ €์žฅ
    3. ์ „ํ™˜ ํšจ๊ณผ OFF (์•„์ดํ…œ ๋“œ๋ž˜๊ทธ์‹œ ์ง€์—ฐ์ด ์—†์–ด์•ผ ํ•˜๋ฏ€๋กœ)

 

์Šค์™€์ดํ”„ ์ง„ํ–‰ ํ•ธ๋“ค๋Ÿฌ

  • ์ด๋ฒคํŠธ ์œ ํ˜• : onTouchMove / onMouseMove
  • ์‹คํ–‰ ์กฐ๊ฑด : swipeStartPos ์ƒํƒœ ๊ฐ’์ด ์กด์žฌํ•จ
  • ํ•ธ๋“ค๋Ÿฌ ์•ก์…˜
    1. ์ด๋ฒคํŠธ ๊ฐ์ฒด์—์„œ ํ˜„์žฌ clientX ์ขŒํ‘œ ํš๋“
    2. swipeStartPos - clientX ๊ฒฐ๊ณผ๊ฐ’ diff ์ƒํƒœ์— ์ €์žฅ
      • ์™ผ์ชฝ์œผ๋กœ ๋“œ๋ž˜๊ทธํ•˜๋ฉด + ๊ฐ’ (์บ๋กœ์…€ ํŠธ๋ž™์„ ์ขŒ์ธก์œผ๋กœ ์ด๋™)
      • ์˜ค๋ฅธ์ชฝ์œผ๋กœ ๋“œ๋ž˜๊ทธํ•˜๋ฉด - ๊ฐ’ (์บ๋กœ์…€ ํŠธ๋ž™์„ ์šฐ์ธก์œผ๋กœ ์ด๋™)

 

์Šค์™€์ดํ”„ ์ข…๋ฃŒ ํ•ธ๋“ค๋Ÿฌ

  • ์ด๋ฒคํŠธ ์œ ํ˜• : onTouchEnd / onMouseUp / onMouseLeave
  • ํ•ธ๋“ค๋Ÿฌ ์•ก์…˜
    1. ์ด์ „ / ๋‹ค์Œ ์•„์ดํ…œ ์ด๋™ ์—ฌ๋ถ€ ํŒ๋‹จ (diff์™€ ๋น„๊ตํ•˜๋Š” ๊ฐ’์ด ํด์ˆ˜๋ก ์Šค์™€์ดํ”„ ๋ฏผ๊ฐ๋„ ํ•˜๋ฝ)
      • diff > 10 : ๋‹ค์Œ(์šฐ์ธก) ์•„์ดํ…œ์œผ๋กœ ์ด๋™
      • diff < -10 : ์ด์ „(์ขŒ์ธก) ์•„์ดํ…œ์œผ๋กœ ์ด๋™
    2. ์ธ๋ฑ์Šค ๋ณ€๊ฒฝ ํ•จ์ˆ˜ moveToNextIndex ์‹คํ–‰ (๋ฐฉํ–ฅ ์ •๋ณด 'next' | 'prev' ์ธ์ž๋กœ ์ „๋‹ฌ)
    3. swipeStartPos ์ƒํƒœ ์ดˆ๊ธฐํ™”(null)
    4. diff ์ƒํƒœ ์ดˆ๊ธฐํ™”(0)

 

์ธ๋ฑ์Šค ๋ณ€๊ฒฝ ํ•จ์ˆ˜

๐Ÿ’ก ์ธ๋ฑ์Šค ๋ณ€๊ฒฝ ํ•จ์ˆ˜๋Š” ์ขŒ/์šฐ ๋ฒ„ํŠผ, ํ•˜๋‹จ Dot์„ ํด๋ฆญํ–ˆ์„ ๋•Œ๋„ ํ˜ธ์ถœ๋œ๋‹ค

moveToNextIndex(type: 'prev' | 'next', waitTransition?: boolean) : void

 

  • ์‹คํ–‰ ์กฐ๊ฑด : ์ „ํ™˜ ์ด๋ฒคํŠธ ์ง„ํ–‰์ค‘์ด ์•„๋‹˜ — isOnTransitionEvent === false
  • ํ•จ์ˆ˜ ์•ก์…˜
    1. ์ „ํ™˜ ํšจ๊ณผ ON (์ด๋™์‹œ ์ „ํ™˜ ํšจ๊ณผ๊ฐ€ ์ ์šฉ๋ผ์•ผ ํ•˜๋ฏ€๋กœ)
    2. currentIndex ์ƒํƒœ ๋ณ€๊ฒฝ
      • ์ด์ „ ์•„์ดํ…œ์œผ๋กœ ์ด๋™(type === 'prev') : currentIndex - 1
      • ๋‹ค์Œ ์•„์ดํ…œ์œผ๋กœ ์ด๋™(type === 'next') : currentIndex + 1

 

currentIndex, diff ์ƒํƒœ๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค ์Šค์™€์ดํ”„ ์‚ฌ์ด์ฆˆ(์บ๋กœ์…€ ์•„์ดํ…œ์˜ x์ถ• ์ด๋™ ๋ฒ”์œ„)๋ฅผ ๊ณ„์‚ฐํ•˜๊ณ  ๊ฒฐ๊ณผ๊ฐ’์€ ์บ๋กœ์…€ ์•„์ดํ…œ ์—˜๋ฆฌ๋จผํŠธ์˜ transform ์Šคํƒ€์ผ ์†์„ฑ(calc ํ•จ์ˆ˜)์— ์ „๋‹ฌํ•œ๋‹ค.

 

์Šค์™€์ดํ”„๊ฐ€ ์ง„ํ–‰์ค‘(๋“œ๋ž˜๊ทธ)์ผ ๋• diff ์ƒํƒœ๊ฐ€ ๊ณ„์† ๋ณ€๊ฒฝ๋ผ์„œ ์บ๋กœ์…€ ์•„์ดํ…œ์ด ์ขŒ/์šฐ๋กœ ์›€์ง์ธ๋‹ค. ์Šค์™€์ดํ”„๋ฅผ ์ข…๋ฃŒํ•˜๋ฉด diff ์ƒํƒœ๋Š” ์ดˆ๊ธฐํ™”๋˜๊ณ  ๋ณ€๊ฒฝ๋œ currentIndex ์ƒํƒœ์— ๋”ฐ๋ผ ์ด์ „ ํ˜น์€ ๋‹ค์Œ ์•„์ดํ…œ์œผ๋กœ ์ด๋™ํ•œ๋‹ค.

๋”๋ณด๊ธฐ
import {
  DetailedHTMLProps,
  HTMLAttributes,
  MouseEvent,
  TouchEvent,
} from 'react';

export type CarouselItemAttributes<T = HTMLDivElement> = DetailedHTMLProps<
  HTMLAttributes<T>,
  T
>;

type TouchEventKeys =
  | 'onTouchStart'
  | 'onTouchMove'
  | 'onTouchEnd'
  | 'onTouchCancel';
type MouseEventKeys =
  | 'onMouseDown'
  | 'onMouseMove'
  | 'onMouseUp'
  | 'onMouseLeave'
  | 'onMouseEnter'
  | 'onMouseOut'
  | 'onMouseOver';

type TrackEvents = TouchEventKeys & MouseEventKeys;
type SwipeEvent<T = HTMLDivElement> = TouchEvent<T> | MouseEvent<T>;

export type TrackEventAttributes = {
  [key in TrackEvents]: (event: SwipeEvent) => void;
};
// useCarousel.tsx
const swipeSizeCalcExpression = `-${currentIndex * 100}% - ${diff}px`;

// ์บ๋กœ์…€ ์•„์ดํ…œ Parent์— ์ „๋‹ฌํ•  Props
const carouselItemProps: CarouselItemAttributes = {
  ref: trackRef, // ์ „ํ™˜ ์‹œ์ž‘ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ ๋“ฑ๋ก. ์ด๋ฒคํŠธ ๋ฐœ์ƒ์‹œ isOnTransitionEvent ์ƒํƒœ true๋กœ ๋ณ€๊ฒฝ
  style: {
    transform: `translateX(calc(${swipeSizeCalcExpression}))`,
    transition: carouselModel.isActiveTransition // ์ „ํ™˜ ํšจ๊ณผ On/Off ์—ฌ๋ถ€
      ? `transform ${options.speed}ms ${options.transitionType}` // currentIndex๊ฐ€ ๋ณ€๊ฒฝ๋ผ์„œ ์•„์ดํ…œ์„ ์ด๋™ํ•  ๋•Œ
      : undefined, // ๋“œ๋ž˜๊ทธ ์ค‘์ผ ๋•Œ
  },
  onTransitionEnd: () => onTransitionEndAfterDelay(), // isOnTransitionEvent ์ƒํƒœ false๋กœ ๋ณ€๊ฒฝ
};

// ์บ๋กœ์…€ ์•„์ดํ…œ Wrapper(์บ๋กœ์…€ ํŠธ๋ž™)์— ์ „๋‹ฌํ•  Props
const trackEventHandlers: TrackEventAttributes = {
  onTouchStart: onSwipeStart,
  onMouseDown: onSwipeStart,
  // ...
};

 

์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ๋Š” ์บ๋กœ์…€ ์•„์ดํ…œ Wrapper(์บ๋กœ์…€ ํŠธ๋ž™) ์š”์†Œ์— ์ „๋‹ฌํ•˜๊ณ , ์ „ํ™˜ ํšจ๊ณผ ๊ด€๋ จ Props๋Š” ์บ๋กœ์…€ ์•„์ดํ…œ Parent ์š”์†Œ์— ์ „๋‹ฌํ•œ๋‹ค. Carousel ์ปดํฌ๋„ŒํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ณณ์—์„œ ์บ๋กœ์…€ ์•„์ดํ…œ์˜ width๋ฅผ ์œ ์—ฐํ•˜๊ฒŒ ์ปจํŠธ๋กคํ•  ์ˆ˜ ์žˆ๋„๋ก React.cloneElement๋ฅผ ์ด์šฉํ•ด ์บ๋กœ์…€ ์•„์ดํ…œ์— width ํ”„๋กญ์„ ์ถ”๊ฐ€ํ•œ๋‹ค.

 

๐Ÿ’ก ์บ๋กœ์…€ ์•„์ดํ…œ Parent ๋ฐ”๋กœ ์•„๋ž˜์—(1depth) ์žˆ๋Š” ๋ชจ๋“  ์ž์‹ ์ปดํฌ๋„ŒํŠธ๋Š” width ํ”„๋กญ์„ ๋ฐ›๊ฒŒ ๋œ๋‹ค

// Carousel.tsx
{ /* ์บ๋กœ์…€ ์•„์ดํ…œ Wrapper (์บ๋กœ์…€ ํŠธ๋ž™) */ }
<div className="overflow-hidden w-full h-full" {...trackEventHandlers}>
  {/* ์บ๋กœ์…€ ์•„์ดํ…œ Parent */}
  <div className="flex no-scrollbar" {...carouselItemProps}>
    {Children.map(carouselChildren, (child, i) => {
      if (isValidElement(child)) {
        return cloneElement(child, {
          ...child.props,
          key: i,
          width: `${carouselOptions.itemWidthRatio}%`,
        });
      }
    })}
  </div>
</div>;

 

  • React.Children.map(children, callback) : ๊ฐ children์— ์ฝœ๋ฐฑ์„ ํ˜ธ์ถœํ•˜๊ณ  ์ƒˆ ๋ฐฐ์—ด ๋ฐ˜ํ™˜. children์ด null, undefined ์ด๋ฉด ๋ฐฐ์—ด์ด ์•„๋‹Œ null, undefined ๊ทธ๋Œ€๋กœ ๋ฐ˜ํ™˜.
  • React.isValidElement(object) : object๊ฐ€ React ์—˜๋ฆฌ๋จผํŠธ์ธ์ง€ ์—ฌ๋ถ€๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” Boolean ๋ฐ˜ํ™˜.
  • React.cloneElement(element, [config], […children]) : element๋ฅผ ๋ณต์ œํ•˜์—ฌ ์ƒˆ๋กœ์šด React ์—˜๋ฆฌ๋จผํŠธ ๋ฐ˜ํ™˜. ์ฃผ๋กœ prop์„ ์ถ”๊ฐ€/์‚ญ์ œํ•˜๊ฑฐ๋‚˜, ์ž์‹ ์ปดํฌ๋„ŒํŠธ ๊ธฐ๋Šฅ ํ™•์žฅ์„ ์œ„ํ•ด ์‚ฌ์šฉ.
    • element : ๋ณต์ œํ•  ์—˜๋ฆฌ๋จผํŠธ
    • config : ๋ณต์ œํ•  ์—˜๋ฆฌ๋จผํŠธ์— ๋„˜๊ธธ props, key, ref. ๋”ฐ๋กœ key, ref ๋ช…์‹œ ์•ˆํ•˜๋ฉด ์›๋ณธ ์œ ์ง€
    • children : ๋ณต์ œํ•  ์—˜๋ฆฌ๋จผํŠธ์˜ ์ž์‹ ์š”์†Œ. ์ถ”๊ฐ€/๋Œ€์ฒด ๊ฐ€๋Šฅ. ๋”ฐ๋กœ ๋ช…์‹œ ์•ˆํ•˜๋ฉด ์›๋ณธ ์œ ์ง€

 

๋ฌดํ•œ ์Šฌ๋ผ์ด๋“œ


๐Ÿ’ก ํ™”๋ฉด์— 1๊ฐœ ์•„์ดํ…œ ํ‘œ์‹œ ๊ธฐ์ค€์œผ๋กœ ์ž‘์„ฑ

 

๊ตฌํ˜„ ๋ฐฉ๋ฒ• ์„ค๋ช… โšก๏ธ

  • ์ฒซ๋ฒˆ์งธ / ๋งˆ์ง€๋ง‰ ์•„์ดํ…œ์„ ๋ณต์ œํ•ด์„œ ์›๋ณธ ์ฒซ๋ฒˆ์งธ ์•„์ดํ…œ ์ „์—” ๋งˆ์ง€๋ง‰ ์•„์ดํ…œ์„, ์›๋ณธ ๋งˆ์ง€๋ง‰ ์•„์ดํ…œ ๋’ค์—” ์ฒซ๋ฒˆ์งธ ์•„์ดํ…œ์„ ์ด์–ด๋ถ™์ธ๋‹ค.
    • ์›๋ณธ ์•„์ดํ…œ : A B C D
    • ์ด์–ด ๋ถ™์ธ ํ›„ ์•„์ดํ…œ : cD (A B C D) cA
      • cD ์ธ๋ฑ์Šค : ๋งˆ์ง€๋ง‰ ์•„์ดํ…œ(D)
      • cA ์ธ๋ฑ์Šค : ์ฒซ๋ฒˆ์งธ ์•„์ดํ…œ(A)
  • ํ˜„์žฌ ์ธ๋ฑ์Šค์—์„œ(D) ๋‹ค์Œ ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ์„œ ๋ณ€๊ฒฝํ•  ์ธ๋ฑ์Šค๊ฐ€ ๋งˆ์ง€๋ง‰(cA)์ด๋ผ๋ฉด…
    • D(๋งˆ์ง€๋ง‰ ์•„์ดํ…œ) → cA(์ฒซ๋ฒˆ์งธ ์•„์ดํ…œ) ์ธ๋ฑ์Šค๋กœ ์ „ํ™˜ํšจ๊ณผ๋ฅผ ์œ ์ง€ํ•˜๋ฉด์„œ ์ด๋™ํ•œ๋‹ค
    • ์ด๋•Œ ํ™”๋ฉด์ƒ์œผ๋ก  ์ฒซ๋ฒˆ์งธ ์•„์ดํ…œ(A)์œผ๋กœ ์ด๋™ํ•˜๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ๋ณด์ธ๋‹ค
    • ์ด๋™์„ ์™„๋ฃŒํ•˜๋ฉด(์ „ํ™˜ Delay ์ดํ›„) ์ „ํ™˜ ํšจ๊ณผ๋ฅผ ์ž ์‹œ Offํ•˜๊ณ  currentIndex๋ฅผ A๋กœ ๋ณ€๊ฒฝํ•œ๋‹ค
    • ์ „ํ™˜ ํšจ๊ณผ๋ฅผ Off ํ–ˆ์œผ๋ฏ€๋กœ ํ™”๋ฉด์ƒ์—” ์•„๋ฌด๋Ÿฐ ๋ณ€ํ™”๊ฐ€ ์—†๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ๋ณด์ธ๋‹ค

 

์ธ๋ฑ์Šค ๋ณ€๊ฒฝ ํ•จ์ˆ˜ ์ˆ˜์ •

์•„๋ž˜์ฒ˜๋Ÿผ ์ธ๋ฑ์Šค ๊ฐ€์ด๋“œ ๋งต์„ ์ƒ์ˆ˜๋กœ ๋งŒ๋“ค์–ด ๋‘๊ณ  ์‚ฌ์šฉํ•˜๋ฉด ๊ตฌํ˜„์‹œ ์‹ค์ˆ˜๋ฅผ ์ค„์ผ ์ˆ˜ ์žˆ๋‹ค.

export const IDX_OFFSET = 1;

export const IDX_GUIDE_MAP = (childrenCount: number) => ({
  FIRST_ITEM: IDX_OFFSET, // ์›๋ณธ ๋ฐฐ์—ด์—์„œ ์ฒซ๋ฒˆ์งธ ์•„์ดํ…œ ์ธ๋ฑ์Šค
  LAST_ITEM: childrenCount - IDX_OFFSET, // ์›๋ณธ ๋ฐฐ์—ด์—์„œ ๋งˆ์ง€๋ง‰ ์•„์ดํ…œ ์ธ๋ฑ์Šค
  TAIL: childrenCount + IDX_OFFSET, // ๋ณต์ œํ•œ ๋ฐฐ์—ด์—์„œ ์ฒซ๋ฒˆ์งธ ์•„์ดํ…œ(๋งˆ์ง€๋ง‰ ์ธ๋ฑ์Šค)
  FRONT: 0, // ๋ณต์ œํ•œ ๋ฐฐ์—ด์—์„œ ๋งˆ์ง€๋ง‰ ์•„์ดํ…œ(์ฒซ๋ฒˆ์งธ ์ธ๋ฑ์Šค)
});

 

์ธ๋ฑ์Šค๋ฅผ ๋ณ€๊ฒฝํ•˜๋Š” moveToNextIndex ํ•จ์ˆ˜์— ๋‹ค์Œ ์ธ๋ฑ์Šค๊ฐ€ ์ฒซ๋ฒˆ์งธ(FRONT) ํ˜น์€ ๋งˆ์ง€๋ง‰(TAIL)์ธ์ง€ ํŒŒ์•…ํ•˜๊ณ  Transition Delay(โถ์ธ๋ฑ์Šค ๋ณ€๊ฒฝ ํ›„ ํŠธ๋ž™ ์ด๋™ ์™„๋ฃŒ) ์ดํ›„ โท๊ต์ •ํ•  ์ธ๋ฑ์Šค๋กœ ๊ต์ฒดํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค.

const moveToNextIndex = (type: CarouselArrow, waitTransition = true) => {
  // carouselModel.isIdle : ์ „ํ™˜ ์ด๋ฒคํŠธ ์ง„ํ–‰์ค‘ ์—ฌ๋ถ€ ์ƒํƒœ isOnTransitionEvent ์กฐํšŒ
  if (waitTransition && !carouselModel.isIdle) return;

  const isPrev = type === 'prev';
  const nextIndex = carouselIndex + (isPrev ? -1 : 1); // ์ด๋™ํ•ด์•ผํ•  ์ธ๋ฑ์Šค

  setCarouselIndexes(nextIndex); // โ‘ด currentIndex ์ƒํƒœ ๋ณ€๊ฒฝ
  carouselModel.onMoveToIdxAction(); // ์ „ํ™˜ ํšจ๊ณผ ON (isActiveTransition ์ƒํƒœ ๋ณ€๊ฒฝ)

  const isFront = nextIndex === IDX_GUIDE.FRONT;
  const isTale = nextIndex === IDX_GUIDE.TAIL;

  if (isFront || isTale) {
    // nextIndex๊ฐ€ 0์ด๋ผ๋ฉด ์›๋ณธ ๋ฐฐ์—ด์˜ ๋งˆ์ง€๋ง‰ ์ธ๋ฑ์Šค๋กœ ์ด๋™ํ•ด์•ผ ํ•œ๋‹ค.
    // ๋ณต์ œํ•œ ๋ฐฐ์—ด์€ ๊ฐ€์žฅ ์•ž์— ์š”์†Œ๊ฐ€ 1๊ฐœ ๋” ์ถ”๊ฐ€๋์œผ๋ฏ€๋กœ + 1์„ ํ•ด์ค€๋‹ค.
    const toIndex = isPrev ? IDX_GUIDE.LAST_ITEM + 1 : 1;
    moveToIdxPromptly(toIndex); // โ‘ต ์ธ๋ฑ์Šค ๊ต์ • ํ•จ์ˆ˜ ์‹คํ–‰
  }
};

 

์ธ๋ฑ์Šค๋ฅผ ๊ต์ •ํ•ด์ฃผ๋Š” moveToIdxPromptly ํ•จ์ˆ˜๋Š” Transition Delay ์ดํ›„(์บ๋กœ์…€ ํŠธ๋ž™ ์ด๋™ ์™„๋ฃŒ), ์ „ํ™˜ ํšจ๊ณผ๋ฅผ ๋„๊ณ  ์‹คํ–‰ํ•˜๋ฏ€๋กœ ํ™”๋ฉด ์ƒ์œผ๋ก  ์•„๋ฌด๋Ÿฐ ์ผ๋„ ์ผ์–ด๋‚˜์ง€ ์•Š์€ ๊ฒƒ์ฒ˜๋Ÿผ ๋ณด์ธ๋‹ค.

const moveToIdxPromptly = (index: number) => {
  setTimeout(() => {
    carouselModel.onMoveToIdxPromptlyAction(); // ์ „ํ™˜ ํšจ๊ณผ OFF (isActiveTransition ์ƒํƒœ ๋ณ€๊ฒฝ)
    setCarouselIndexes(index); // currentIndex๋ฅผ ๊ต์ •ํ•  ์ธ๋ฑ์Šค๋กœ ๋ณ€๊ฒฝ
    onTransitionEndAfterDelay(); // isOnTransitionEvent(์ „ํ™˜ ์ด๋ฒคํŠธ ์ง„ํ–‰์ค‘ ์—ฌ๋ถ€) ์ƒํƒœ false๋กœ ๋ณ€๊ฒฝ
  }, options.speed); // Transition Delay ms
};

 

๋ ˆํผ๋Ÿฐ์Šค


 


๊ธ€ ์ˆ˜์ •์‚ฌํ•ญ์€ ๋…ธ์…˜ ํŽ˜์ด์ง€์— ๊ฐ€์žฅ ๋น ๋ฅด๊ฒŒ ๋ฐ˜์˜๋ฉ๋‹ˆ๋‹ค. ๋งํฌ๋ฅผ ์ฐธ๊ณ ํ•ด ์ฃผ์„ธ์š”
๋ฐ˜์‘ํ˜•