Recoil

Recoil์ด๋ž€?

Facebook์—์„œ ๋งŒ๋“  React ์ƒํƒœ ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ž…๋‹ˆ๋‹ค. Recoil์€ ์ž์‹ ์˜ ์žฅ์ ์„ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์„ค๋ช…ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

  • ๋ฆฌ์ฝ”์ผ์€ ๋ฆฌ์•กํŠธ์ฒ˜๋Ÿผ ์ผํ•˜๊ณ  ์ƒ๊ฐํ•œ๋‹ค. ์•ฑ์— ์ผ๋ถ€๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ  ๋น ๋ฅด๊ณ  ์œ ์—ฐํ•œ ๊ณต์œ  ์ƒํƒœ๋ฅผ ์œ ์ง€ํ•ด๋ผ.
  • ํŒŒ์ƒ ๋ฐ์ดํ„ฐ์™€ ๋น„๋™๊ธฐ ์ฟผ๋ฆฌ๋Š” ์ˆœ์ˆ˜ํ•œ ๊ธฐ๋Šฅ๊ณผ ํšจ์œจ์ ์ธ ๊ตฌ๋…์— ๊ธธ๋“ค์—ฌ์ง„๋‹ค.
  • ์ฝ”๋“œ ๋ถ„ํ• ์„ ์†์ƒ์‹œํ‚ค์ง€ ์•Š๊ณ  ์•ฑ ์ „์ฒด์—์„œ ๋ชจ๋“  ์ƒํƒœ ๋ณ€๊ฒฝ์„ ๊ด€์ฐฐํ•˜์—ฌ ์ง€์†์„ฑ, ๋ผ์šฐํŒ…, ์‹œ๊ฐ„ ์ด๋™ ๋””๋ฒ„๊น… ๋˜๋Š” ์‹คํ–‰ ์ทจ์†Œ๋ฅผ ๊ตฌํ˜„ํ•ด๋ผ.

๊ทธ๋ ‡๋‹ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

๋“ฑ์žฅ ๋ฐฐ๊ฒฝ

๋ณต์žกํ•œ react ์•ฑ์—์„œ๋Š” ์ƒํƒœ ๊ด€๋ฆฌ๊ฐ€ ๊ฐœ๋ฐœ์ž์˜ ์ฃผ์š”ํ•˜๊ณ  ์–ด๋ ค์šด ์ž‘์—…์ด ๋ฉ๋‹ˆ๋‹ค. ํ•˜๋‚˜์˜ ์ƒํƒœ ๋ณ€๊ฒฝ์œผ๋กœ ์ธํ•ด ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ๋“ค์— ์˜ํ–ฅ์„ ์ฃผ๊ธฐ๋„ ํ•˜๊ธฐ ๋•Œ๋ฌธ์ธ๋ฐ, ๊ทธ๋ž˜์„œ Redux๋‚˜ MobX๊ฐ™์€ ์ƒํƒœ ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์ด ๋“ฑ์žฅํ•˜๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
Redux๋‚˜ MobX์˜ API๋Š” ๋‹จ์ˆœํ•˜์ง€ ์•Š๊ณ , ๋ณธ ๋ชฉ์ ์ด React์—์„œ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด ๋‚˜์˜จ๊ฒƒ์ด ์•„๋‹™๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ API์™€ ๋™์ž‘ ๋ฐฉ์‹์„ ๊ฐ€๋Šฅํ•œ ๋ฆฌ์•กํŠธ์Šค๋Ÿฝ๊ฒŒ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด ๋ฆฌ์ฝ”์ผ์„ ์ œ์ž‘ํ–ˆ๋‹ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

์ ์šฉ๊ธฐ

์ผ๋‹จ CRA๋กœ ์•ฑ ํ•˜๋‚˜๋ฅผ ๋งŒ๋“ค์–ด ์ค๋‹ˆ๋‹ค.

npx create-react-app recoil-practice

๊ทธ๋ฆฌ๊ณ  ๋ฆฌ์ฝ”์ผ์„ ์ธ์Šคํ†จ ํ•ฉ๋‹ˆ๋‹ค.

cd recoil-practice
npm i recoil

or

yarn add recoil

RecoilRoot

ํ”„๋กœ์ ํŠธ์˜ ๋ฃจํŠธ ์ปดํฌ๋„ŒํŠธ์— RecoilRoot๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. ๋ฆฌ์ฝ”์ผ ๋ฃจํŠธ๋ฅผ ์„ค์ •ํ•ด์ฃผ์–ด์•ผ ๋‚ด๋ถ€์—์„œ ๋ฆฌ์ฝ”์ผ ์‚ฌ์šฉ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ํƒ€ ์ƒํƒœ๊ด€๋ฆฌ์˜ provider์™€ ๋น„์Šทํ•œ ์—ญํ• ์„ ํ•˜๋Š” ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.
ํ”„๋กœ์ ํŠธ์˜ index.js์— ์ถ”๊ฐ€ํ•ด์ค๋‹ˆ๋‹ค.

import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
import * as serviceWorker from './serviceWorker'
import { RecoilRoot } from 'recoil'

ReactDOM.render(
  <React.StrictMode>
    <RecoilRoot>
      <App />
    </RecoilRoot>
  </React.StrictMode>,
  document.getElementById('root')
)

ํ•ต์‹ฌ ๊ฐœ๋…

Recoil์€ Atom์œผ๋กœ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด์„œ Selector๋ฅผ ๊ฑฐ์ณ React ์ปดํฌ๋„ŒํŠธ๊นŒ์ง€ ์ „๋‹ฌ๋˜๋Š” ํ•˜๋‚˜์˜ Data-Flow Graph๋ฅผ ๋งŒ๋“ค๊ฒŒ ํ•œ๋‹ค. Atom์€ ์ปดํฌ๋„ŒํŠธ๋“ค์ด ๊ตฌ๋…ํ•  ์ˆ˜ ์žˆ๋Š” ๋‹จ์œ„์ด๊ณ , Selector๋Š” ๋™๊ธฐ์  ํ˜น์€ ๋น„๋™๊ธฐ์ ์œผ๋กœ ์ƒํƒœ๋ฅผ ๋ณ€ํ™˜ํ•œ๋‹ค. ์ด ๋‘๊ฐ€์ง€์˜ ํ•ต์‹ฌ ๊ฐœ๋…์œผ๋กœ ์ด๋ฃจ์–ด์ง„ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ด๋‹ค.

๊ตฌ์„ฑ ์š”์†Œ๋“ค

  • atom ์•„ํ†ฐ์€ ์ƒํƒœ์˜ ์กฐ๊ฐ์„ ๋‚˜ํƒ€๋‚ด๋ฉฐ key์™€ value๋ฅผ ์„ค์ •ํ•ด์ค€๋‹ค. ๊ธฐ๋ณธ ์ƒํƒœ์ด๋‹ค.

    import { atom } from 'recoil'
    
    const number = atom({
    key: 'number',
    default: 0,
    })

    ๋ฐฐ์—ด, ๊ฐ์ฒด๋ฅผ ๊ฐ’์œผ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

    import { atom } from 'recoil'
    
    const todos = atom({
    key: 'todos',
    default: ['ํ™”์žฅ์‹ค ๊ฐ€๊ธฐ', '๋ฐฅ๋จน๊ธฐ'],
    })
  • Selector ์…€๋ ‰ํ„ฐ๋Š” reducer์™€ ๋น„์Šทํ•˜๋‹ค. ์…€๋ ‰ํ„ฐ์—์„œ atom์˜ ๊ฐ’์„ ๊ฐ€์ ธ์˜ค๊ณ , switch๋ฌธ ๋“ฑ์œผ๋กœ ๋งค๊ฐœ๋ณ€์ˆ˜์˜ ํ•ด๋‹น ๋ถ€๋ถ„์„ ์ฐพ์•„, ๊ฐ’์„ ๋ณ€ํ˜•์‹œ์ผœ ๋ฐ˜ํ™˜ํ•  ์ˆ˜ ์žˆ๋‹ค. ๋‹ค๋ฅธ atom ๋˜๋Š” selector๋ฅผ ๋ฐ›์•„ ์‚ฌ์šฉํ•˜๋Š” ์ˆœ์ˆ˜ ํ•จ์ˆ˜. ์ž‘์„ฑํ•  ๋•Œ ํ•จ์ˆ˜์— ๊ณ ์œ ํ•œ ํ‚ค์™€ ๊ฒŒํ„ฐ(get), ์„ธํ„ฐ(set)์„ ์ „๋‹ฌํ•˜์—ฌ ์ž‘์„ฑํ•œ๋‹ค. ๊ฒŒํ„ฐ๋Š” ํ•„์ˆ˜์ง€๋งŒ ์„ธํ„ฐ๋Š” ํ•„์ˆ˜๊ฐ€ ์•„๋‹ˆ๋‹ค.
  • useRecoilState useState์™€ ํก์‚ฌํ•˜๊ฒŒ, [value, setValue]ํ˜•์‹์œผ๋กœ setValue๋กœ ๊ฐ’์„ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๋‹ค.
  • useRecoilValue: ์˜ค์ง recoil์˜ ๊ฐ’๋งŒ ๊ฐ€์ ธ์˜ค๊ณ  ์‹ถ์„ ๋•Œ ์‚ฌ์šฉํ•œ๋‹ค.

todoList ์ปดํฌ๋„ŒํŠธ

๊ณต์‹๋ฌธ์„œ์— ์žˆ๋Š” ํˆฌ๋‘๋ฆฌ์ŠคํŠธ ์˜ˆ์ œ๋ฅผ ๋”ฐ๋ผํ•ด๋ณด๋ฉฐ ์ ์šฉํ•˜๊ธฐ ์œ„ํ•ด todoList์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

import React from 'react'
import { atom, useRecoilValue } from 'recoil'

const todoListState = atom({
  key: 'todoListState',
  default: [],
})

function TodoList() {
  const todoList = useRecoilValue(todoListState)

  return (
    <>
      {/* <TodoListStats /> */}
      {/* <TodoListFilters /> */}
      <TodoItemCreater />
      {todoList.map(todoItem => (
        <TodoItem key={todoItem.id} item={todoItem} />
      ))}
    </>
  )
}

export default TodoList

TodoItemCreator

iimport React, { useState } from 'react';
import { useSetRecoilState } from 'recoil';
import { todoListState } from '../recoilState';

function TodoItemCreator() {
  const [inputValue, setInputValue] = useState('');
  const setTodoList = useSetRecoilState(todoListState);

  const additem = () => {
    setTodoList(oldTodoList => [
      ...oldTodoList,
      {
        id: getId(),
        text: inputValue,
        isComplete: false
      }
    ]);
    setInputValue('');
  };

  const onChange = ({ target: { value } }) => {
    setInputValue(value);
  };

  return (
    <div>
      <input type="text" value={inputValue} onChange={onChange} />
      <button onClick={additem}>Add</button>
    </div>
  );
}

export default TodoItemCreator;

let id = 0;
function getId() {
  return id++;
}

useSetRecoilStateํ›…์„ ์‚ฌ์šฉํ•ด์„œ ์•„ํ†ฐ์œผ๋กœ ์ •์˜ํ•œ ์ƒํƒœ๋ฅผ ์ธ์ž๋กœ ๋„ฃ์–ด์ฃผ๋ฉด ์„ธํ„ฐ ํ•จ์ˆ˜๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. ์„ธํ„ฐ ํ•จ์ˆ˜๋ฅผ ๊ฐ€์ ธ์˜จ ๋’ค, ์ธํ’‹ ์ƒํƒœ๊ด€๋ฆฌ๋ฅผ ๋‚ด๋ถ€์ƒํƒœ๋กœ ๊ด€๋ฆฌํ•˜๊ณ  ํˆฌ๋‘๋ฆฌ์ŠคํŠธ ๋‚ด์šฉ ์ถ”๊ฐ€ ํ•จ์ˆ˜์— ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

TodoItem

function TodoItem({ item }) {
  const [todoList, setTodoList] = useRecoilState(todoListState)
  const index = todoList.findIndex(listItem => listItem === item)

  const editItemText = ({ target: { value } }) => {
    const newList = replaceItemAtIndex(todoList, index, {
      ...item,
      text: value,
    })

    setTodoList(newList)
  }

  const toggleItemCompletion = () => {
    const newList = replaceItemAtIndex(todoList, index, {
      ...item,
      isComplete: !item.isComplete,
    })

    setTodoList(newList)
  }

  const deleteItem = () => {
    const newList = removeItemAtIndex(todoList, index)

    setTodoList(newList)
  }

  return (
    <div>
      <input type="text" value={item.text} onChange={editItemText} />
      <input
        type="checkbox"
        checked={item.isComplete}
        onChange={toggleItemCompletion}
      />
      <button onClick={deleteItem}>X</button>
    </div>
  )
}

export default TodoItem

function replaceItemAtIndex(arr, index, newValue) {
  return [...arr.slice(0, index), newValue, ...arr.slice(index + 1)]
}

function removeItemAtIndex(arr, index) {
  return [...arr.slice(0, index), ...arr.slice(index + 1)]
}

TodoItem ์ปดํฌ๋„ŒํŠธ์—์„œ๋Š” useRecoilStateํ›…์„ ์ผ๋ฐ˜ useStateํ›…์ฒ˜๋Ÿผ ์‚ฌ์šฉํ•ด ์ƒํƒœ๊ฐ’๊ณผ ์„ธํ„ฐํ•จ์ˆ˜๋ฅผ ๊ฐ€์ ธ์™”์Šต๋‹ˆ๋‹ค. ์ผ๋ฐ˜ ํˆฌ๋‘๋ฆฌ์ŠคํŠธ ์˜ˆ์ œ ๋‚ด์šฉ๊ณผ ๋น„์Šทํ•œ๋ฐ, delete, edit๋ฐฉ์‹์„ ํŽธํ•˜๊ฒŒ ReplaceItemAtIndex์™€ ๊ฐ™์ด ํ•จ์ˆ˜๋ฅผ ๋”ฐ๋กœ ๋นผ์–ด์„œ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

export const todoListFilterState = atom({
  key: 'todoListFilterState',
  default: 'Show All',
})

๋ชฉ๋ก์„ ํ•„ํ„ฐ๋งํ•  ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด todoListFilterState๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

export const filteredTodoListState = selector({
  key: 'filteredTodoListState',
  get: ({ get }) => {
    const filter = get(todoListFilterState)
    const list = get(todoListState)

    switch (filter) {
      case 'Show Completed':
        return list.filter(item => item.isComplete)
      case 'Show Uncompleted':
        return list.filter(item => !item.isComplete)
      default:
        return list
    }
  },
})

filteredTodoListState๋ผ๋Š” ํ•จ์ˆ˜์— selector๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ‚ค์™€ ๊ฒŒํ„ฐ ํ•จ์ˆ˜๋ฅผ ์ „๋‹ฌํ•˜์—ฌ ์ž‘์„ฑํ•˜์˜€์Šต๋‹ˆ๋‹ค.
todoListFilterState์™€ todoListState ๋‘ ๊ฐ€์ง€ ์ƒํƒœ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ฐ€๊ณต๋œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š”๊ฒƒ ์ด ๋ชฉ์ ์ž…๋‹ˆ๋‹ค. ๊ฐ€๊ณต๋œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›๊ฑฐ๋‚˜ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€๊ณตํ•˜์—ฌ ์ €์žฅํ•˜๊ณ  ์‹ถ์„๋•Œ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
ํ•ด๋‹น ์…€๋ ‰ํ„ฐ์—์„œ todoListFilterState์™€ todoListState๋ฅผ ํŠธ๋ž˜ํ‚นํ•˜์—ฌ, ๋ณ€ํ™”๊ฐ€ ๊ฐ์ง€๋˜๋ฉด ๋‹ค์‹œ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.

function TodoList() {
  const todoList = useRecoilValue(filteredTodoListState)

  return (
    <>
      <TodoListStats />
      <TodoListFilters />
      <TodoItemCreator />
      {todoList.map(todoItem => (
        <TodoItem key={todoItem.id} item={todoItem} />
      ))}
    </>
  )
}

export default TodoList

ํˆฌ๋‘ ๋ฆฌ์ŠคํŠธ ์ปดํฌ๋„ŒํŠธ์˜ useRecoilValue์—์„œ todolistState์ƒํƒœ์—์„œ ๊ฐ€์ ธ์˜ค๋˜ ๊ฐ’์„ filteredTodoListState์ƒํƒœ์—์„œ ๊ฐ€์ ธ์˜ค๋„๋ก ๋ฐ”๊ฟ”์ค๋‹ˆ๋‹ค.

TodoListFilters

import React from 'react'
import { useRecoilState } from 'recoil'
import { todoListFilterState } from '../recoilState'

function TodoListFilters() {
  const [filter, setFilter] = useRecoilState(todoListFilterState)

  const updateFilter = ({ target: { value } }) => {
    setFilter(value)
  }

  return (
    <>
      Filter:
      <select value={filter} onChange={updateFilter}>
        <option value="Show All">All</option>
        <option value="Show Completed">Completed</option>
        <option value="Show Uncompleted">Uncompleted</option>
      </select>
    </>
  )
}

export default TodoListFilters

ํ•„ํ„ฐ๋ง์„ ์„ ํƒํ•  ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋„ฃ์–ด์ค๋‹ˆ๋‹ค. ์…€๋ ‰ํŠธ๊ฐ€ ์„ ํƒ๋˜๋ฉด ํ•ด๋‹น ๊ฐ’์œผ๋กœ todoListFilterState์˜ ์ƒํƒœ๋ฅผ ๋ฐ”๊ฟ‰๋‹ˆ๋‹ค.

export const todoListStatsState = selector({
  key: 'todoListStatsState',
  get: ({ get }) => {
    const todoList = get(todoListState)
    const totalNum = todoList.length
    const totalCompletedNum = todoList.filter(item => item.isComplete).length
    const totalUncompletedNum = totalNum - totalCompletedNum
    const percentCompleted = totalNum === 0 ? 0 : totalCompletedNum / totalNum

    return {
      totalNum,
      totalCompletedNum,
      totalUncompletedNum,
      percentCompleted,
    }
  },
})

๋ฆฌ์ŠคํŠธ์˜ ํ† ํƒˆ ์ˆ˜, ํผ์„ผํ‹ฐ์ง€๋ฅผ ํ‘œํ˜„ํ•˜๊ธฐ์œ„ํ•ด todoListState์˜ ์ƒํƒœ๋ฅผ ๊ฐ€๊ณตํ•ด ๋ฐ˜ํ™˜ํ•˜๋Š” ์…€๋ ‰ํ„ฐ ํ•˜๋‚˜๋ฅผ ์ž‘์„ฑํ•ด์ค๋‹ˆ๋‹ค.

TodoListStats

import React from 'react'
import { useRecoilValue } from 'recoil'
import { todoListStatsState } from '../recoilState'

function TodoListStats() {
  const {
    totalNum,
    totalCompletedNum,
    totalUncompletedNum,
    percentCompleted,
  } = useRecoilValue(todoListStatsState)

  const formattedPercentCompleted = Math.round(percentCompleted * 100)

  return (
    <ul>
      <li>Total items: {totalNum}</li>
      <li>Items completed: {totalCompletedNum}</li>
      <li>Items not completed: {totalUncompletedNum}</li>
      <li>Percent completed: {formattedPercentCompleted}</li>
    </ul>
  )
}

export default TodoListStats

์œ„์—์„œ ์ •์˜ํ•œ ์Šคํƒฏ ์ƒํƒœ๋ฅผ ๊ฐ€์ ธ์™€์„œ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด TodoListStats ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์—์„œ๋„ useRecoilValue๋ฅผ ์‚ฌ์šฉํ•ด ๋ฆฌ์ฝ”์ผ ์‚ณํƒœ์˜ ๊ฐ’์„ ๊ฐ€์ ธ์™”์Šต๋‹ˆ๋‹ค.

์ด์ œ npm run start๋ฅผ ํ•ด์„œ ์‹คํ–‰์‹œ์ผœ ๋ณด๋ฉด, ๊ฐ ํˆฌ๋‘๋ฆฌ์ŠคํŠธ์˜ ํ•ฉ๊ณ„๊ฐ€ ๋‚˜์˜ค๊ณ , ํ•„ํ„ฐ๋ง์ด ์žˆ์œผ๋ฉฐ ํ• ๋ฐ€๋ชฉ๋ก๋“ค์˜ ๋‚ด์šฉ์ด ์ž˜ ๋‚˜์˜ค๋Š”๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
๊ธฐ๋ณธ์ ์ธ ํ• ๋ฐ€๋ชฉ๋ก ์˜ˆ์ œ๋ฅผ ๋”ฐ๋ผํ•ด๋ณด๋ฉฐ ์–ด๋А ์ •๋„ Recoil์˜ ๊ธฐ๋ณธ์ ์ธ ์‚ฌ์šฉ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด์„  ์•Œ๊ฒ ์œผ๋‚˜, ํŠนํžˆ ์–ด๋–ค์ ์—์„œ ์ข‹์€์ง€๋Š” ์•„์ง ์ž˜ ๋ชจ๋ฅด๊ฒ ์Šต๋‹ˆ๋‹ค. ์ผ๋‹จ ์‚ฌ์šฉ๋ฒ•์ด ๋ฆฌ๋•์Šค์— ๋น„ํ•ด ๊ฐ„ํŽธํ•˜๊ณ  ์‰ฌ์šด ๊ฒƒ ๊ฐ™์œผ๋ฉฐ ํ›…์ฒ˜๋Ÿผ ์‚ฌ์šฉํ•˜๊ธฐ์— ์ต์ˆ™ํ•œ ์ ๋„ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๊ฒƒ๋งŒ์œผ๋กœ๋„ ํฐ ์žฅ์ ์ด ๋˜๊ฒ ์ง€๋งŒโ€ฆ ๋‹ค์Œ ๋น„๋™๊ธฐ ๋ฐ์ดํ„ฐ ๋‹ค๋ฃจ๊ธฐ๋„ ํ•œ๋ฒˆ ์ง„ํ–‰ํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.


jaeyoung-son
Written by@jaeyoung-son
๋ฐฐ์šด๊ฒƒ์„ ๊ธฐ๋กํ•˜๋Š” ๊ณต๊ฐ„์ž…๋‹ˆ๋‹ค.

GitHub