lil.dev
Published on

๐ŸŽ‰ ์ปฌ๋Ÿฌ๋ฆฌ์ŠคํŠธ ํ”„๋กœ์ ํŠธ #12 Pallete table - resizable columns

๊ธ€์“ด์ด

    ๐Ÿ“Œ ๋ชฉ์ฐจ

    Welcome

    โœจ ์ปฌ๋Ÿฌ๋ฆฌ์ŠคํŠธ ์‚ฌ์ดํŠธ(์•„์ง ์—†์Œ

    ๐Ÿ’๐Ÿป

    1. ์„ ํƒ์นธ ํ…Œ์ด๋ธ” width ์กฐ์ ˆํ•˜๊ฒŒ ํ•ด๋ณด๊ธฐ -'resizable columns' 1-1. ๋‹ค์‹œ ๋งŒ๋“œ๋Š” resizable columns

    ์„ ํƒ์นธ ํ…Œ์ด๋ธ” width ์กฐ์ ˆํ•˜๊ฒŒ ํ•ด๋ณด๊ธฐ

    ์„ ํƒ์นธ ํญ ๋ณ€๊ฒฝํ•˜๊ธฐ

    ์‹œ๋‚˜๋ฆฌ์˜ค

    Scenario: ์„ ํƒ์นธ์˜ ๊ฐ€๋กœ ๊ธธ์ด๋ฅผ ๋งˆ์Œ๋Œ€๋กœ ๋Š˜๋ฆฌ๊ฑฐ๋‚˜ ์ค„์ผ ์ˆ˜ ์žˆ๋‹ค
      Given ์„ ํƒ์นธ์„ ๋ Œ๋”ํ•˜๊ณ 
      When ์›ํ•˜๋Š” ์นธ์„ ์„ ํƒํ•ด์„œ ์žก์•„๋‹น๊ธฐ๋ฉด
      Then ์„ ํƒ์นธ์„ ์ค„์ด๊ฑฐ๋‚˜ ๋Š˜์ผ ์ˆ˜ ์žˆ๋‹ค
    

    1-1. ๋‹ค์‹œ ๋งŒ๋“œ๋Š” resizable columns

    ์ปฌ๋Ÿฌ๋ฆฌ์ŠคํŠธ์˜ 2๊ต์‹œ ์‹œํ—˜์€
    ์ƒ‰์ข…์ด๋ฅผ ๋‹ค์–‘ํ•œ ๋น„์œจ๋กœ ์ž˜๋ผ
    ๋ฌธ์ œ์— ๋งž๋Š” ์กฐํ™”๋กœ์šด ๋ฐฐ์ƒ‰์„ ํ‘œํ˜„ํ•˜๋Š” ์‹œํ—˜์ž…๋‹ˆ๋‹ค.

    ๋ณดํ†ต ์•„๋ž˜์™€ ๊ฐ™์€ ๋ชจ์Šต์œผ๋กœ ์™„์„ฑ๋˜๋Š”๋ฐ์š”.

    Resizable columns-1

    ์ด๋ ‡๊ฒŒ ์—ฌ๋Ÿฌ ์ƒ‰์˜ ์ข…์ด๋ฅผ ๋‹ค์–‘ํ•œ ๋„ˆ๋น„๋กœ ์ž˜๋ผ ๋ถ™์ด๋Š” ๊ณผ์ •์„
    ์›น์—์„œ๋„ ํ•  ์ˆ˜ ์žˆ๋„๋ก
    ์‚ฌ์ด์ฆˆ๋ฅผ ์กฐ์ ˆํ•  ์ˆ˜ ์žˆ๋Š” ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ์„ ๋งŒ๋“ค๋ ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค..!

    ๊ทธ๋Ÿผ ๋จผ์ € ํ…Œ์ด๋ธ”์„ ๋งŒ๋“ค๊ธฐ ์ „ ์กฐ๊ฑด๋“ค์„ ํ™•์ธํ•ด์•ผ ํ•˜๋Š”๋ฐ์š”,

    ์ผ๋‹จ ์ „์ฒด ๋ฐ•์Šค์˜ ํฌ๊ธฐ๋Š” ๋ฐ–์—์„œ ์ฃผ์ž…ํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    ์˜ˆ๋ฅผ ๋“ค์–ด ์ „์ฒด ํฌ๊ธฐ๋ฅผ 500ํ”ฝ์…€์งœ๋ฆฌ ๋ฐ•์Šค๋กœ ๋งŒ๋“ค์–ด ๋ณผ๊ฒŒ์š”.

    Resizable columns-2

    ๋ฐ•์Šค๋ฅผ 1/4, 1/4, 1/2์˜ ๋น„์œจ์„ ๊ฐ€์ง„ ์„ธ ๊ฐœ์˜ ์นธ์œผ๋กœ ๋‚˜๋ˆ„์–ด๋ด…๋‹ˆ๋‹ค.

    Resizable columns-3

    ์—ฌ๊ธฐ์„œ ์ดˆ๋ก ๋ฐ•์Šค์˜ ์˜ค๋ฅธ์ชฝ ๋ชจ์„œ๋ฆฌ๋ฅผ ํด๋ฆญํ•˜๋ฉด?

    Resizable columns-4

    ๊ฐ€๋งŒํžˆ ์žˆ์—ˆ๋˜ ๋ฐ•์Šค์˜ ์ƒํƒœ๋ฅผ โ€˜๋ณ€๊ฒฝ ์ค‘ ๋ชจ๋“œโ€™๋กœ ๋ฐ”๊พธ๊ณ 
    ํด๋ฆญ์„ ํ•œ ์ˆœ๊ฐ„์˜ ๋ชจ์„œ๋ฆฌ x์ขŒํ‘œ๋ฅผ โ€˜์‹œ์ž‘ ์ขŒํ‘œโ€™๋กœ ๊ธฐ๋กํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค!

    ๊ทธ๋ž˜์•ผ ํด๋ฆญ์„ ํ•˜์ง€ ์•Š๋Š” ์ˆœ๊ฐ„์—๋Š” ๋ฐ•์Šค ๋„ˆ๋น„ ๋ณ€๊ฒฝ์ด ๋˜์ง€ ์•Š๊ณ ,
    ๋˜ ๋ฐ•์Šค ๋ชจ์„œ๋ฆฌ์˜ ์œ„์น˜๊ฐ€ ๋ฐ”๋€Œ์—ˆ์„ ๋•Œ ๋ฐ”๋€ ์ขŒํ‘œ์™€ โ€˜์‹œ์ž‘ ์ขŒํ‘œโ€™๋ฅผ ๋น„๊ตํ•ด์„œ
    ๋ชจ์„œ๋ฆฌ๊ฐ€ ์›€์ง์ธ ๊ฑฐ๋ฆฌ๋ฅผ ๊ตฌํ•ด ๋ฐ•์Šค ๋„ˆ๋น„๋ฅผ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ด์ฃ !

    ์ดˆ๋ก ๋ฐ•์Šค์˜ ์˜ค๋ฅธ์ชฝ ๋ชจ์„œ๋ฆฌ๋ฅผ ํด๋ฆญํ•œ ๋‹ค์Œ ๋“œ๋ž˜๊ทธ๋กœ ๋งˆ์šฐ์Šค๋ฅผ ์›€์ง์ด๋ฉด?

    Resizable columns-5

    ๋ฐ”๋€ ์ดˆ๋ก ์ƒ์ž์˜ ์˜ค๋ฅธ์ชฝ ๋ชจ์„œ๋ฆฌ x ์ขŒํ‘œ ๊ฐ’์„ ์•Œ์•„์•ผํ•˜๊ณ ,
    ์ฒ˜์Œ โ€˜์‹œ์ž‘ ์ขŒํ‘œโ€™์™€ โ€˜๋ฐ”๋€ ์ขŒํ‘œโ€™ ์‚ฌ์ด์˜ ์ฆ๊ฐ๊ฐ’ = โ€˜๋‘ ์ขŒํ‘œ ๊ฐ„ ๊ฑฐ๋ฆฌโ€™๋„ ์•Œ์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    -> ์ดˆ๋ก ์ƒ์ž๊ฐ€ ์ปค์ง€๋ฉด ๋‘ ์ขŒํ‘œ ์‚ฌ์ด์˜ ๊ฑฐ๋ฆฌ๋Š” ์–‘์ˆ˜(+)์ด๊ณ 
    -> ์ดˆ๋ก ์ƒ์ž๊ฐ€ ์ค„์–ด๋“ค๋ฉด ๋‘ ์ขŒํ‘œ ์‚ฌ์ด์˜ ๊ฑฐ๋ฆฌ๋Š” ์Œ์ˆ˜(-)๊ฒ ์ฃ ?

    ์ด ๋•Œ 2๊ฐ€์ง€ ๋” ์‹ ๊ฒฝ์จ์•ผ ํ•˜๋Š” ์‚ฌํ•ญ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

    ์ฒซ๋ฒˆ์งธ๋Š”, ๋งˆ์šฐ์Šค๋ฅผ ํด๋ฆญํ•œ ํ›„ ์›€์ง์ด๋Š” ์ƒํ™ฉ(๋ณ€๊ฒฝ ์ค‘ ๋ชจ๋“œ)์—์„œ
    ๋ฆฌ์•กํŠธ์˜ useState๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ~๋ฐฐ์—ด์— ๋ณ€๊ฒฝ ์ค‘ ๋ชจ๋“œ ์ƒํƒœ๋ฅผ ๋„ฃ์–ด์ฃผ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    ๊ทธ๋ณด๋‹ค ๋จผ์ €, ํด๋ฆญ ํ›„ ๋“œ๋ž˜๊ทธ๋ฅผ ํ•œ ํ›„ ๋งˆ์šฐ์Šค๋ฅผ ๋†“์œผ๋ฉด(mouseUp) ๋”์ด์ƒ ๋ณ€๊ฒฝ์ด ๋˜์ง€ ์•Š๋„๋ก
    ์ƒํƒœ๊ฐ’์„ false๋กœ ๋ฐ”๊ฟ”์ฃผ์–ด์•ผ ํ•˜๊ตฌ์š”. ๊ทธ ๋‹ค์Œ ๋ณ€๊ฒฝ ์ค‘ ๋ชจ๋“œ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ์ƒํƒœ์ธ โ€˜isChangingโ€™์„
    ์˜์กด์„ฑ ๋ฐฐ์—ด์— ๋„ฃ์–ด์ฃผ๋ฉด ๋“œ๋ž˜๊ทธ๊ฐ€ ๋๋‚ฌ์„ ๋•Œ๋Š” ๋” ์ด์ƒ ์ดˆ๋ก ์ƒ์ž์˜ ๋„ˆ๋น„๊ฐ€ ๋ณ€๊ฒฝ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

    ๋‘๋ฒˆ์งธ๋Š”, ๋งˆ์šฐ์Šค๋ฅผ ์›€์ง์ผ ์ˆ˜ ์žˆ๋Š” ์ตœ๋Œ€ ๊ฑฐ๋ฆฌ์™€ ์ตœ์†Œ ๊ฑฐ๋ฆฌ๋ฅผ ๋”ฐ์ ธ์ฃผ์–ด์•ผ ํ•˜๋Š” ๊ฒƒ์ธ๋ฐ์š”.
    ๋งŒ์•ฝ ์ด ๋‘ ๊ฑฐ๋ฆฌ๋ฅผ ์ •ํ•ด์ฃผ์ง€ ์•Š๋Š”๋‹ค๋ฉด, ์ดˆ๋ก ์ƒ์ž์˜ ํฌ๊ธฐ๊ฐ€ ํŒŒ๋ž€ ์ƒ์ž๋ฅผ ์นจ๋ฒ”ํ•ด ์‚ผ์ผœ๋ฒ„๋ฆฌ๊ฑฐ๋‚˜
    ๋…ธ๋ž€ ์ƒ์ž๊ฐ€ ์‚ผ์ผœ์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
    ์šฐ๋ฆฌ๋Š” ์ด๋ฏธ ๋งŒ๋“ค์–ด๋†“์€ ์ƒ์ž๋Š” ์‚ผ์ผœ์ง€๊ธฐ๋ฅผ ๋ฐ”๋ผ์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์—,
    ํ•œ ์ƒ์ž๊ฐ€ ๊ฐ€์งˆ ์ˆ˜ ์žˆ๋Š” ์ตœ๋Œ€ ๊ฑฐ๋ฆฌ์™€ ์ตœ์†Œ ๊ฑฐ๋ฆฌ๋ฅผ ์ •ํ•ด์ค„ ๊ฑฐ์—์š”.

    ์ผ๋‹จ ํ•œ ์ƒ์ž๊ฐ€ ๊ฐ€์งˆ ์ˆ˜ ์žˆ๋Š” ์ตœ์†Œ ๋„ˆ๋น„๋ฅผ 8px๋กœ ์ •ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

    ๊ทธ๋Ÿผ ์œ„์˜ ์ดˆ๋ก ์ƒ์ž๊ฐ€ ์˜ค๋ฅธ์ชฝ์œผ๋กœ ์ญˆ์šฑ ๋Š˜์–ด๋‚ฌ์„ ๋•Œ

    Resizable columns-6

    ์ดˆ๋ก ์ƒ์ž์˜ ์ตœ๋Œ€ ๋„ˆ๋น„๋Š” ๋ช‡์ด ๋ ๊นŒ์š”?
    ๊ธฐ์กด์˜ ์ดˆ๋ก ์ƒ์ž๊ฐ€ ๊ฐ€์ง„ ๋„ˆ๋น„์™€ ํŒŒ๋ž€ ์ƒ์ž๊ฐ€ ๊ฐ€์ง„ ๋„ˆ๋น„์˜ ์ดํ•ฉ์—์„œ
    ํŒŒ๋ž€ ์ƒ์ž๊ฐ€ ๊ฐ€์ ธ์•ผํ•˜๋Š” ์ตœ์†Œ ๋„ˆ๋น„์ธ 8px์„ ๋บ€ ๊ฐ’์ด ๋˜๊ฒ ์ฃ .

    Resizable columns-7

    ๋ฐ˜๋Œ€๋กœ ์ดˆ๋ก ์ƒ์ž์˜ ์ตœ์†Œ ๋„ˆ๋น„๋Š” ๊ทธ๋Ÿผ ๋ช‡์ด ๋ ๊นŒ์š”?
    ์šฐ๋ฆฌ๊ฐ€ ์ •ํ•ด๋‘” ์ƒ์ž์˜ ์ตœ์†Œ ๋„ˆ๋น„ 8px์ด ๋˜๊ฒ ์ฃ .

    ์ด๊ฑธ ์ฝ”๋“œ๋กœ ํ‘œํ˜„ํ•˜์ž๋ฉด ์ด๋ ‡๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

    if (isChanging) {
      const diff = e.clientX - startX
    
      const left = oldSegments[changingIndex]
      const right = oldSegments[changingIndex + 1]
    
      const maxRatio = left + right - 8
    
      const diffAsRatio = diff / width
      const newLeft = Math.max(Math.min(left + diffAsRatio, maxRatio), 8)
      const newRight = Math.max(Math.min(right - diffAsRatio, maxRatio), 8)
      const newSegments = [...oldSegments]
      newSegments[changingIndex] = newLeft
      newSegments[changingIndex + 1] = newRight
      setSegments(newSegments)
      console.log(newSegments)
    }
    

    ์ฝ”๋“œ

    import React, { useEffect, useState } from 'react'
    
    const width = 512
    
    // ๊ตฌ์กฐ ๋ถ„ํ•ด ํ• ๋‹น
    // https://beta.reactjs.org/learn/passing-props-to-a-component
    function ResizableBoxes({
      colors,
      deleteSelected,
    }: {
      colors: string[]
      deleteSelected: (targetIndex: number) => void
    }) {
      const BOX_COUNT = colors.length
    
      const [segments, setSegments] = useState<number[]>(Array(BOX_COUNT).fill(1 / BOX_COUNT))
      const [oldSegments, setOldSegments] = useState<number[]>(Array(BOX_COUNT).fill(1 / BOX_COUNT))
      const [startX, setStartX] = useState(0)
      const [changingIndex, setChangingIndex] = useState(0)
      const [isChanging, setIsChanging] = useState(false)
    
      useEffect(() => {
        //๋งˆ์šฐ์Šค๊ฐ€ ์›€์ง์ผ ๋•Œ ๋งˆ๋‹ค ๋ฐ•์Šค ํฌ๊ธฐ๋ฅผ ์กฐ์ ˆํ•ด์ฃผ๋Š” ํ•ธ๋“ค๋Ÿฌ
        function moveHandler(e: MouseEvent) {
          if (isChanging) {
            // ์ฒ˜์Œ ์‹œ์ž‘ํ•  ๋•Œ ์™ผ์ชฝ ๋ฐ•์Šค๋ž‘, ์˜ค๋ฅธ์ชฝ ๋ฐ•์Šค ํฌ๊ธฐ๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค
            const left = oldSegments[changingIndex]
            const right = oldSegments[changingIndex + 1]
    
            // ์™ผ์ชฝ ์˜ค๋ฅธ์ชฝ ๋ฐ•์Šค ๋„ˆ๋น„์˜ ํ•ฉ์—์„œ...
            // ์ตœ๋Œ€๋Š”? ํ•ฉ - 1/16
            // ์ตœ์†Œ 1/16
            const maxRatio = left + right - 1 / 16
    
            // ํ˜„์žฌ ๋งˆ์šฐ์Šค ์œ„์น˜์™€, ๊พน ๋ˆ„๋ฅด๊ธฐ ์‹œ์ž‘ํ•œ ์œ„์น˜์™€์˜ ์ฐจ์ด
            const diff = e.clientX - startX
            // ๋งˆ์šฐ์Šค๊ฐ€ ์›€์ง์ธ ๋ณ€์œ„๊ฐ€... ์ „์ฒด ํฌ๊ธฐ์—์„œ ๋น„์œจ๋กœ ์–ผ๋งˆ๋ฅผ ์ฐจ์ง€ํ•˜๋Š”์ง€?
            const diffAsRatio = diff / width
    
            // const ์ตœ๋Œ“๊ฐ’ = 1 / 2;
            // Math.min(1, ์ตœ๋Œ“๊ฐ’); // => 1/2 ์ตœ๋Œ“๊ฐ’๊นŒ์ง€ ๊ฐ€๋Šฅ!
            // Math.min(1 / 3, 1 / 2); // => 1/3
    
            // const ์ตœ์†Ÿ๊ฐ’ = 1 / 16;
            // Math.max(1 / 2, ์ตœ์†Ÿ๊ฐ’); // => 1/2
            // Math.max(0, ์ตœ์†Ÿ๊ฐ’); // => 1/16
    
            const newLeft = Math.max(Math.min(left + diffAsRatio, maxRatio), 1 / 16)
            const newRight = Math.max(Math.min(right - diffAsRatio, maxRatio), 1 / 16)
            const newSegments = [...oldSegments]
            newSegments[changingIndex] = newLeft
            newSegments[changingIndex + 1] = newRight
            setSegments(newSegments)
          }
        }
    
        // 1. ๋งˆ์šฐ์Šค๋ฅผ ๋–ผ์—ˆ์„ ๋•Œ... ๋” ์ด์ƒ ๋ณ€๊ฒฝ๋˜์ง€ ์•Š๊ฒŒ ํ•ด์ฃผ๋Š” ์นœ๊ตฌ
        function upHandler() {
          // 2.
          setIsChanging(false)
        }
        // ์ด ๋‘ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ์œˆ๋„์šฐ(ํ™”๋ฉด)์— ๋‹ฌ์•„์คŒ
        window.addEventListener('mousemove', moveHandler)
        window.addEventListener('mouseup', upHandler)
    
        // cleanup => 3.
        return () => {
          // ํ•ธ๋“ค๋Ÿฌ๋ฅผ ์œˆ๋„์šฐ์—์„œ ๋–ผ์–ด์คŒ...
          window.removeEventListener('mousemove', moveHandler)
          window.removeEventListener('mouseup', upHandler)
        }
        // ์˜์กด์„ฑ ๋ฐฐ์—ด์— isChanging์ด false๋กœ ๋ณ€ํ•˜๋ฉด?
      }, [isChanging, oldSegments, startX])
    
      return (
        <table className="selectedContainer flex flex-row">
          <tr className="flex flex-row w-1/2 h-48">
            {segments.map((segment, i) => (
              <td
                className="flex flex-row p-0"
                key={i}
                style={{
                  width: `${segment * 100}%`,
                  backgroundColor: colors[i],
                }}
              >
                <div className="h-full w-11/12" onClick={() => deleteSelected(i)}>
                  {colors[i]}
                </div>
                {i < segments.length - 1 && (
                  <div
                    className="bg-blue-600 w-0.5 hover:w-2 transition-all p-0 m-0 h-full cursor-pointer"
                    //์‹œ์ž‘ ์ขŒํ‘œ๋ฅผ ๊ธฐ๋กํ•˜๊ธฐ ์œ„ํ•ด ๋ชจ์„œ๋ฆฌ๋ฅผ ๊พน ๋ˆ„๋ฅด๋ฉด
                    onMouseDown={(e) => {
                      console.log('x', e.clientX, 'y', e.clientY)
                      setStartX(e.clientX) // ์‹œ์ž‘ x์ขŒํ‘œ
                      setChangingIndex(i) // ๋ฐ”๊พธ๋ ค๋Š” ๊ทธ ์ธ๋ฑ์Šค
                      setOldSegments(segments) // ์‹œ์ž‘ํ•  ๋•Œ ๋ฐ•์Šค ํฌ๊ธฐ
                      setIsChanging(true) // ๋ณ€๊ฒฝ ์ค‘์ž„์„ ์•Œ๋ ค์ฃผ๋Š” flag
                    }}
                  ></div>
                )}
              </td>
            ))}
          </tr>
        </table>
      )
      // https://tailwindcss.com/docs/cursor
    }
    
    export default ResizableBoxes