๐Ÿ”ฎ ์†Œ๋งˆ๋ฒ• ํ”„๋กœ์ ํŠธ -7 (avengers)

๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋ž ๊ธฐ๋Šฅ์„ ๊ฐ„๋‹จํ•˜๊ฒŒ ๊ตฌํ˜„ํ•œ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜.

react-beautiful-dnd ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ–ˆ๊ณ , ์–ด๋ฒค์ ธ์Šค ์บ๋ฆญํ„ฐ๋“ค์„ ์ปจํ…์ธ ๋กœ ์‚ฌ์šฉํ•ด๋ณด์•˜๋‹ค.


#. Project Map


์ œ์ž‘๋…ธํŠธ ํ•œ๋ˆˆ์—๋ณด๊ธฐ[์ ‘๊ธฐ/ํŽผ์น˜๊ธฐ]

1. ๋ ˆ์ด์•„์›ƒ

1-1. ๋ฉ”์ธํ™”๋ฉด

avengers

ํ™”๋ฉด ๊ตฌ์„ฑ์€ ๋“œ๋ž˜๊ทธ์•ค๋“œ๋ž ๊ธฐ๋Šฅ์ด ๋“ค์–ด์žˆ๋Š” ์ƒ๋‹จ ์ปจํ…Œ์ด๋„ˆ์™€ ๋ฐ์ดํ„ฐ๋ฅผ ํ™”๋ฉด์— ๊ทธ๋ ค์ฃผ๋Š” ํ•˜๋‹จ ์ปจํ…Œ์ด๋„ˆ, ํฌ๊ฒŒ ๋‘ ๊ฐœ๋กœ ๊ตฌ์„ฑํ•˜์˜€๋‹ค.

1-2. ์ปดํฌ๋„ŒํŠธ

<BackgroundContainer>
  <ContentsMenubar name="avengers" data={dndData} />
  <Description>Try Drag & Drop</Description>
  <TopContainer>
    <DragDropContext>
      <DndContainer>{/* ๋“œ๋ž˜๊ทธ์•ค ๋“œ๋ž ๊ด€๋ จ code */}</DndContainer>
    </DragDropContext>
  </TopContainer>
  <Description>Make them friends</Description>
  <div className="mobile_description">scroll >>> </div>
  <BottomContainer>{/* ๋ฐ์ดํ„ฐ -> ๋ทฐ code */}</BottomContainer>
</BackgroundContainer>

ContentsMenubar : ๋ฆฌ๋“€์„œ์— ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๊ฑฐ๋‚˜ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฆฌ์…‹์‹œํ‚ฌ ์ˆ˜ ์žˆ๋Š” ๋ฉ”๋‰ด๋ฐ”

Description : ์„ค๋ช… (styled-component)

TopContainer : ๋“œ๋ž˜๊ทธ์•ค ๋“œ๋ž ๊ธฐ๋Šฅ, react-beautiful-dnd ์˜ DragDropContext API๋ฅผ ์‚ฌ์šฉํ–ˆ๋‹ค.

div.mobile_description : width๊ฐ€ BottomContainer ๋ณด๋‹ค ์ž‘์•„์ง€๋ฉด ์Šคํฌ๋กคํ•˜๋ผ๋Š” ์„ค๋ช…๋„ ๋ณด์ด๋„๋ก ํ•ด์ฃผ์—ˆ๋‹ค.

BottomContainer : ๋ฐ์ดํ„ฐ๋ฅผ ํ™”๋ฉด์— ๊ทธ๋ ค์ฃผ๋Š” ์—ญํ• 

2. ์ƒํƒœ๊ด€๋ฆฌ

const data = useSelector(state => state.avengers)
const [dndData, setDndData] = useState(data)

2-1. ๋ฆฌ์•กํŠธ state

๋ฆฌ์•กํŠธ hooks๋ฅผ ์ด์šฉํ•ด์„œ ์ปดํฌ๋„ŒํŠธ ๋‚ด์—์„œ์˜ ์ƒํƒœ๊ด€๋ฆฌ๋ฅผ ํ•˜์˜€๋‹ค.

๊ทธ๋ฆฌ๊ณ  ๋“œ๋ž˜๊ทธ์•ค ๋“œ๋ž์œผ๋กœ ๋ฐ์ดํ„ฐ์˜ ๋ณ€๊ฒฝ์‹œ, ๋ถˆ๋ณ€์„ฑ์„ ์œ ์ง€์‹œ์ผœ์ฃผ๋ฉฐ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ”๊พธ์–ด์ฃผ์—ˆ๋‹ค.

dndData : ๊ฐ์ฒดํ˜•ํƒœ์˜ ํ˜„์žฌ column, item๋“ค์˜ ๋ฐ์ดํ„ฐ

2-2. ๋ฆฌ๋•์Šค store

๋ฆฌ๋•์Šค hooks๋ฅผ ์ด์šฉํ–ˆ๋‹ค.

contentsMenuBar์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๋Š” ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด useDispatch๋ฅผ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๊ณ 

useSelector๋กœ ์ €์žฅ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์™”๋‹ค.

// reducers > avengers.js

const initialState = {
  items: {
    'item-1': { id: 'item-1', content: 'Captain America', src: 'captain.jpg' },
    'item-2': { id: 'item-2', content: 'IonMan', src: 'ironman.jpg' },
    'item-3': { id: 'item-3', content: 'Thor', src: 'thor.jpg' },
    'item-4': { id: 'item-4', content: 'Hulk', src: 'hulk.jpg' },
    'item-5': { id: 'item-5', content: 'Spiderman', src: 'spider.jpg' },
    'item-6': { id: 'item-6', content: 'Groot', src: 'groot.jpg' },
    'item-7': { id: 'item-7', content: 'Rocket', src: 'rocket.jpg' },
    'item-8': { id: 'item-8', content: 'Thanos', src: 'thanos.png' },
  },
  columns: {
    'column-1': {
      id: 'column-1',
      title: 'Heros',
      itemIds: [
        'item-1',
        'item-2',
        'item-3',
        'item-4',
        'item-5',
        'item-6',
        'item-7',
      ],
    },
    'column-2': {
      id: 'column-2',
      title: 'villain',
      itemIds: ['item-8'],
    },
  },
  columnOrder: ['column-1', 'column-2'],
}

avengers ๋ฆฌ๋“€์„œ์˜ ์ดˆ๊ธฐ state

3. react-beauifult-dnd

3-1. DragDropContext

https://github.com/atlassian/react-beautiful-dnd

<DragDropContext
  onDragStart={onDrageStartHandler}
  onDragUpdate={onDrageUpdateHandler}
  onDragEnd={onDrageEndHandler}
>
  <DndContainer>
    {dndData.columnOrder.map(columnId => {
      const column = dndData.columns[columnId]
      const items = column.itemIds.map(itemId => dndData.items[itemId])

      return <Column key={column.id} column={column} items={items} />
    })}
  </DndContainer>
</DragDropContext>

๐ŸŽ onDrageStartHandler

dnd ์‹œ์ž‘์‹œ์ 

const onDrageStartHandler = () => {
  for (
    let i = 0;
    i < document.querySelectorAll('.droppable_table').length;
    i++
  ) {
    document.querySelectorAll('.droppable_table')[
      i
    ].style.background = `rgba(255,141,217,0.2)`
  }
}

TopConatiner์—์„œ ๋“œ๋ž˜๊ทธ์•ค๋“œ๋ž์ด ๋ฐœ์ƒํ•˜๋Š” column ๋ถ€๋ถ„์˜ class๋ฅผ โ€˜.droppable_tableโ€™๋กœ ์ •์˜ํ•ด์ฃผ๊ณ  dnd๊ฐ€ ์‹œ์ž‘๋˜๋ฉด ๊ทธ column ๋ฐฐ๊ฒฝ์ƒ‰์„ ๋ณ€๊ฒฝํ•ด์ฃผ์—ˆ๋‹ค.


๐ŸŽ onDrageUpdateHandler

dnd ์—…๋ฐ์ดํŠธ๋˜๋Š” ์‹œ์ 

const onDrageUpdateHandler = update => {
  // ...
}

๋“œ๋ž˜๊ทธ์•ค๋“œ๋ž ์ค‘, ๋“œ๋ž˜๊ทธ ๋„์ค‘ ๋ฐœ์ƒํ•˜๋Š” ํ•จ์ˆ˜. ์‚ฌ์šฉํ•˜์ง€ ์•Š์Œ.


๐ŸŽ onDrageEndHandler

dnd ์ข…๋ฃŒ์‹œ์ 

const onDrageEndHandler = result => {
  document.body.style.color = 'inherit'
  document.body.style.background = 'inherit'
  for (
    let i = 0;
    i < document.querySelectorAll('.droppable_table').length;
    i++
  ) {
    document.querySelectorAll('.droppable_table')[
      i
    ].style.background = `inherit`
  }
  const { destination, source, draggableId } = result
  if (!destination) {
    return
  }
  if (
    destination.droppableId === source.droppableId &&
    destination.index === source.index
  ) {
    return
  }
  const start = dndData.columns[source.droppableId] // 'column-1'
  const finish = dndData.columns[destination.droppableId]

  if (start === finish) {
    const newItemIds = Array.from(start.itemIds)
    newItemIds.splice(source.index, 1)
    newItemIds.splice(destination.index, 0, draggableId)
    const newColumn = {
      ...start,
      itemIds: newItemIds,
    }
    const newDndData = {
      ...dndData,
      columns: {
        ...dndData.columns,
        [newColumn.id]: newColumn,
      },
    }
    setDndData(newDndData)
    return
  }

  const startItemIds = Array.from(start.itemIds)
  startItemIds.splice(source.index, 1)
  const newStart = {
    ...start,
    itemIds: startItemIds,
  }

  const finishItemIds = Array.from(finish.itemIds)
  finishItemIds.splice(destination.index, 0, draggableId)
  const newFinish = {
    ...finish,
    itemIds: finishItemIds,
  }

  const newDndData = {
    ...dndData,
    columns: {
      ...dndData.columns,
      [newStart.id]: newStart,
      [newFinish.id]: newFinish,
    },
  }
  setDndData(newDndData)
  return
}

๋“œ๋ž˜๊ทธ ๊ฐ€๋Šฅํ•œ ์•„์ดํ…œ์„ ๋“œ๋ž˜๊ทธํ•ด์„œ ๋“œ๋žํ•˜๋ฉด ๋ฐœ์ƒํ•˜๋Š” ํ•จ์ˆ˜

ํ•จ์ˆ˜๊ฐ€ ์‹คํ–‰๋  ๋•Œ, result๋ผ๋Š” ๊ฐ์ฒด๋ฅผ ์ „๋‹ฌ๋ฐ›๋Š”๋‹ค. result๊ฐ์ฒด ๋‚ด๋ถ€๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ƒ๊ฒผ๋‹ค.

const result = {
  // ๋“œ๋ž˜๊ทธ ํ•œ item ID
  draggableId: '',
  // ๋“œ๋ž˜๊ทธ ์‹œ์ž‘ ์‹œ์  column ์ •๋ณด
  source: {
    index: 2,
    droppableId: '',
  },
  // ๋“œ๋ž˜๊ทธ ์ข…๋ฃŒ ์‹œ์  column ์ •๋ณด
  destination: {
    index: 1,
    droppableId: '',
  },
}

์ด ์ •๋ณด๋ฅผ ํ†ตํ•ด์„œ ์‹ค์ œ ๋ฐ์ดํ„ฐ๋ฅผ ์—…๋ฐ์ดํŠธํ•ด์ฃผ์—ˆ๋‹ค.

๋จผ์ €, ๋“œ๋ž˜๊ทธ ์ข…๋ฃŒ ์‹œ, ์‹œ์ž‘์ ๊ณผ ๋ชฉ์ ์ง€๊ฐ€ ๋™์ผํ•  ๊ฒฝ์šฐ๋Š” ์กฐ๊ฑด๋ฌธ ์ฒ˜๋ฆฌ๋ฅผ ํ†ตํ•ด ๋‹ค์Œ ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•˜์ง€ ์•Š๊ฒŒ๋” ํ•ด์ฃผ์—ˆ๊ณ 

์‹ค์ œ ๋ณ€๊ฒฝ์ด ์ผ์–ด๋‚ฌ์„ ๊ฒฝ์šฐ, ๋™์ผํ•œ column์—์„œ ๋ณ€๊ฒฝ์ด ์ผ์–ด๋‚ฌ์„ ๋•Œ์™€ ๋‹ค๋ฅธ column์œผ๋กœ ์ด๋™ํ–ˆ์„ ๊ฒฝ์šฐ, ๋‘๊ฐ€์ง€๋ฅผ ์กฐ๊ฑด๋ฌธ์„ ํ†ตํ•ด ๋”ฐ๋กœ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด์ฃผ์—ˆ๋‹ค.


๋™์ผํ•œ column์—์„œ ๋ณ€๊ฒฝ

const newItemIds = Array.from(start.itemIds)
newItemIds.splice(source.index, 1)
newItemIds.splice(destination.index, 0, draggableId)
const newColumn = {
  ...start,
  itemIds: newItemIds,
}
const newDndData = {
  ...dndData,
  columns: {
    ...dndData.columns,
    [newColumn.id]: newColumn,
  },
}
setDndData(newDndData)
return

๋ฆฌ์•กํŠธ์—์„œ ์ƒํƒœ ๋ณ€๊ฒฝ์„ ๊ฐ์ง€ํ•˜๊ธฐ ์œ„ํ•ด์„œ ๊ฐ์ฒด๋ฅผ ์ง์ ‘ ๋ณ€๊ฒฝํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹Œ ์ƒˆ๋กœ์šด ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค์–ด์„œ hooks๋กœ ๋„ฃ์–ด์ฃผ์–ด์•ผํ•œ๋‹ค.

flux ํŒจํ„ด

๊ทธ๋ž˜์„œ ๊ฐ์ฒด ๋‚ด๋ถ€ column ๋ฐฐ์—ด์„ ๋ณ€๊ฒฝํ•ด ์ฃผ์–ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ƒˆ๋กœ์šด ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜๊ณ  source ์ •๋ณด์™€ destination ์ •๋ณด์— ๋งž๊ฒŒ ๋ฐฐ์—ด์„ ์—…๋ฐ์ดํŠธ ํ•ด์ค€ ํ›„์—, ์Šคํ”„๋ ˆ๋“œ ๋ฌธ๋ฒ•์œผ๋กœ ๋ถˆ๋ณ€์„ฑ์„ ์œ ์ง€ํ•˜๋ฉด์„œ hooks๋กœ ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธ ํ•ด์ฃผ์—ˆ๋‹ค.


๋‹ค๋ฅธ column์œผ๋กœ ์ด๋™

const startItemIds = Array.from(start.itemIds)
startItemIds.splice(source.index, 1)
const newStart = {
  ...start,
  itemIds: startItemIds,
}

const finishItemIds = Array.from(finish.itemIds)
finishItemIds.splice(destination.index, 0, draggableId)
const newFinish = {
  ...finish,
  itemIds: finishItemIds,
}

const newDndData = {
  ...dndData,
  columns: {
    ...dndData.columns,
    [newStart.id]: newStart,
    [newFinish.id]: newFinish,
  },
}
setDndData(newDndData)
return

๋“œ๋ž˜๊ทธํ•œ item์ด ๋‹ค๋ฅธ column์œผ๋กœ ์ด๋™ํ–ˆ์„ ๊ฒฝ์šฐ๋Š” column์„ ๊ฐ๊ฐ ์ˆ˜์ •ํ•˜๊ณ  ์—…๋ฐ์ดํŠธํ•ด์ฃผ์—ˆ๋‹ค.

3-2. Droppable

// column.js

const Column = ({ column, items }) => {
  return (
    <Container>
      <Title>{column.title}</Title>
      <Droppable droppableId={column.id}>
        {provided => (
          <ItemList
            className="droppable_table"
            ref={provided.innerRef}
            {...provided.droppableProps}
          >
            {items.map((item, i) => (
              <Item key={item.id} item={item} index={i} />
            ))}
            {provided.placeholder}
          </ItemList>
        )}
      </Droppable>
    </Container>
  )
}

export default Column

droppableId

required property

droppableId๋Š” ํ•„์ˆ˜ ์†์„ฑ์œผ๋กœ, ์ „๋‹ฌ๋ฐ›์€ column props์˜ id ๋ฅผ ๋„ฃ์–ด์ฃผ์—ˆ๋‹ค.


๐ŸŽ provided

react-beautiful-dnd ์˜ Droppable ์ปดํฌ๋„ŒํŠธ๋Š” react ํ•จ์ˆ˜ํ˜•ํƒœ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

The React children of a <Droppable /> must be a function that returns a ReactElement.

<Droppable droppableId="droppable-1">
  {(provided, snapshot) => ({
    /*...*/
  })}
</Droppable>

๊ทธ๋ฆฌ๊ณ  provided ๋ณ€์ˆ˜์˜ ์†์„ฑ ๋ฐ ๋ฉ”์†Œ๋“œ๋ฅผ ํ•จ์ˆ˜ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ์˜ props๋กœ ์ „๋‹ฌํ•ด์ฃผ์—ˆ๋‹ค.

์ž์„ธํ•œ ์šฉ๋ฒ• react-beautiful-dnd Droppable ๋ฉ”๋‰ด์–ผ(๊นƒํ—ˆ๋ธŒ)


3-3. Draggable

// item.js

const Item = ({ item, index }) => {
  return (
    <Draggable draggableId={item.id} index={index}>
      {(provided, snapshot) => (
        <Container
          {...provided.draggableProps}
          {...provided.dragHandleProps}
          ref={provided.innerRef}
          isDragging={snapshot.isDragging}
        >
          {item.content}
        </Container>
      )}
    </Draggable>
  )
}

export default Item

draggableId, index

required property

draggableId์™€ index ๋˜ํ•œ ํ•„์ˆ˜ ์†์„ฑ์œผ๋กœ, ์ „๋‹ฌ๋ฐ›์€ item.id์™€ index๋ฅผ ๋„ฃ์–ด์ฃผ์—ˆ๋‹ค.


๐ŸŽ snapshot

provided ๊ฐ€ ํ™˜๊ฒฝ์— ๊ด€๋ จ๋œ ์š”์†Œ๋ผ๋ฉด snapshot์€ ์ธํ„ฐ๋ ‰์…˜์— ๊ด€๋ จ๋œ ์š”์†Œ์ธ ๊ฒƒ ๊ฐ™๋‹ค. ์—ฌ๋Ÿฌ ์šฉ๋ฒ•์ด ์žˆ๋Š”๋ฐ ์—ฌ๊ธฐ์„œ๋Š” isDragging ์†์„ฑ์— ๋„ฃ์–ด์ฃผ์—ˆ๋‹ค.

์ž์„ธํ•œ ์šฉ๋ฒ• react-beautiful-dnd Draggable ๋ฉ”๋‰ด์–ผ(๊นƒํ—ˆ๋ธŒ)


4. ๋ทฐ (View)

4-1. ๋“œ๋ž˜๊ทธ์•ค๋“œ๋ž ์ปจํ…Œ์ด๋„ˆ

<DndContainer>
  {dndData.columnOrder.map(columnId => {
    const column = dndData.columns[columnId]
    const items = column.itemIds.map(itemId => dndData.items[itemId])

    return <Column key={column.id} column={column} items={items} />
  })}
</DndContainer>

๋จผ์ € DndContainer styled-component ๋กœ ๊ฐ์‹ธ์ฃผ์—ˆ๊ณ , column ๋ฐฐ์—ด์„ map ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด ๋‚˜๋ˆ„๊ณ , ๊ฐ column์— ๋Œ€ํ•œ item ์ •๋ณด๋ฅผ ๋‹ค์‹œ items ๋ฐฐ์—ด์— ๋„ฃ์–ด์ฃผ์—ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ  column๊ณผ items ๋ฅผ ๊ฐ€์ง€๊ณ  Column ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ฆฌํ„ดํ•ด์ฃผ์—ˆ๋‹ค.

4-2. ์ด๋ฏธ์ง€ ์ปจํ…Œ์ด๋„ˆ

<div>
  {dndData.columns['column-1'].itemIds.map((v, i) => {
    return (
      <img
        key={i}
        style={{
          borderRadius: '10px',
          width: '90px',
          height: '90px',
          padding: '5px',
        }}
        src={dndData.items[v].src}
      />
    )
  })}
</div>

column ํ•˜๋‚˜์— ๋Œ€ํ•œ ๋ทฐ (์ด 2๊ฐœ ์žˆ์Œ)

column์˜ items ์ •๋ณด๋“ค์„ map ํ•จ์ˆ˜๋ฅผ ์ด์šฉํ•ด์„œ ๊ฐ๊ฐ์˜ ์ •๋ณด๋“ค์„ img ํƒœ๊ทธ ์†์„ฑ์— ๋‹ด์•„ ๋ฆฌํ„ดํ•ด์ฃผ์—ˆ๋‹ค.

4-3. VS ์ปจํ…Œ์ด๋„ˆ

{
  dndData.columns['column-1'].itemIds.length === 0 ||
  dndData.columns['column-2'].itemIds.length === 0 ? (
    <VSContainer>
      Friend <span>โค๏ธ</span>
    </VSContainer>
  ) : (
    <VSContainer>vs</VSContainer>
  )
}

๋งˆ์ง€๋ง‰์œผ๋กœ column ๊ณผ column ์‚ฌ์ด์— VS ํ…์ŠคํŠธ๋ฅผ ๋„ฃ์—ˆ๋Š”๋ฐ,

๊ฐ column ์ค‘์—์„œ ํ•˜๋‚˜๊ฐ€ item์„ ๊ฐ€์ง€๊ณ  ์žˆ์ง€ ์•Š๊ฒŒ๋˜๋ฉด Friend ๋ผ๋Š” ํ…์ŠคํŠธ๊ฐ€ ์ถœ๋ ฅ๋˜๋„๋ก ์‚ผํ•ญ์—ฐ์‚ฐ์ž๋ฅผ ํ†ตํ•ด ํ•ด์ฃผ์—ˆ๋‹ค.

5. ๊ฐœ์ธ์ ์ธ ํ”ผ๋“œ๋ฐฑ

5-1. ์•„์‰ฌ์šด ๋“œ๋ž˜๊ทธ์•ค๋“œ๋ž ๊ธฐ๋Šฅ

์ด ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์€ ์ƒ๋‹จ์— ๋“œ๋ž˜๊ทธ์•ค๋“œ๋ž ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ์žˆ๊ณ  ํ•˜๋‹จ์— ๊ทธ ์ •๋ณด์— ๋งž๋Š” ์ด๋ฏธ์ง€๊ฐ€ ์กด์žฌํ•˜๋Š”๋ฐ ์•„์‹ธ๋ฆฌ ์ด๋ฏธ์ง€๋ฅผ ๋“œ๋ž˜๊ทธ์•ค๋“œ๋ž ํ•ด์„œ ์ปจํ…Œ์ด๋„ˆ๋ฅผ ํ•˜๋‚˜๋กœ ํ†ตํ•ฉ์‹œํ‚ค๋Š” ๊ฒƒ์ด ๋” ๊ดœ์ฐฎ์„๊ฒƒ ๊ฐ™๋‹ค๋Š” ์ƒ๊ฐ์„ ํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค.

์ฐธ๊ณ ์‚ฌ์ดํŠธ ๋กค์ฒด์ง€์ง€ (๋กคํ† ์ฒด์Šค ๋ฐฐ์น˜ํˆด)


Written by@taenyKim
์›น ํ”„๋ก ํŠธ์—”๋“œ ๊ณต๋ถ€ ๋ธ”๋กœ๊ทธ / Learn in Public

GitHubFacebook