πŸ§‘β€πŸ’» μ§§μ€ν˜Έν‘/React

[React] λ‹€μ‹œ λ§Œλ“œλŠ” todo list (νˆ¬λ‘λ©”μ΄νŠΈ 클둠) (4 - atomFamily, νˆ¬λ‘ 색깔 μ±„μš°κΈ°)

ν•œκ·œμ§„ 2022. 11. 25. 15:59

λŒ€λž΅μ μΈ κΈ°λŠ₯ κ΅¬ν˜„μ΄ λͺ¨λ‘ λλ‚¬μŠ΅λ‹ˆλ‹€. ν•˜λ£¨μ΄ν‹€λ™μ•ˆ κΈ‰ν•˜κ²Œ λ§ˆλ¬΄λ¦¬ν–ˆμŒ. μ •λ¦¬ν•˜μžλ©΄,

  • todoItem을 μœ μ €μ™€ λ‚ μ§œμ— 따라 λ³„λ„μ˜ μƒνƒœλ‘œ κ΄€λ¦¬ν•œλ‹€.
  • 할일을 μ™„λ£Œν•  λ•Œλ§ˆλ‹€ ν•΄λ‹Ή λ‚ μ§œμ˜ 달λ ₯에 색깔과 남은 κ°œμˆ˜κ°€ μ—…λ°μ΄νŠΈλœλ‹€.

 

1. TodoItem κΈ°λŠ₯ ν™•μž₯

달λ ₯ κΈ°λŠ₯ 전에 λ¨Όμ € νˆ¬λ‘ ν”Όλ“œλ₯Ό κ΅¬ν˜„ν•˜λ©΄μ„œ λ‚ μ§œλ₯Ό μƒκ°ν•˜μ§€ μ•Šκ³  λ‹¨μˆœν•˜κ²Œ ν•˜λ‚˜μ˜ μƒνƒœλ‘œλ§Œ κ΄€λ¦¬ν–ˆλ‹€. 이젠 μ„ νƒν•œ λ‚ μ§œμ™€ μ„ νƒν•œ ν”„λ‘œν•„μ— 따라 νˆ¬λ‘λ“€μ„ κ΄€λ¦¬ν•˜κ³  λ³΄μ—¬μ€˜μ•Όν–ˆλ‹€. λ§Žμ€ 고민을 ν–ˆμŒ.

 

μ²˜μŒμ—” 백단 λ””λΉ„μ—μ„œ κ΄€λ¦¬ν•œλ‹€λ©΄ μ–΄λ–»κ²Œ λ˜μ–΄μžˆμ„μ§€λ₯Ό μƒκ°ν–ˆλ‹€ (μ›λž˜λŠ” μ„œλ²„μ—μ„œ 받아와야 ν•˜λŠ” 데이터λ₯Ό ν”„λ‘œμ νŠΈμ—μ„  λͺ©λ°μ΄ν„°μ²˜λŸΌ μ‚¬μš©ν•˜λŠ” 것이기 λ•Œλ¬Έμ—). 'νˆ¬λ‘' ν…Œμ΄λΈ”μ„ ν•˜λ‚˜ 두고, userId와 date μ»¬λŸΌμ„ μΆ”κ°€ν•΄μ„œ κ·Έκ±Έ 톡해 정보λ₯Ό νŒ¨μΉ­ν•΄μ˜€μ§€ μ•Šμ•˜μ„κΉŒ?

 

export interface ITodoItem {
  label: string;
  id: string;
  isDone: boolean;
  category: ICategory;
  userId:string;
  date:string;
}

export const todoState = atom<ITodoItem[]>({
  key: 'todo',
  default: initialState,
});

κΈ°μ‘΄κ³Ό 크게 바꾸지 μ•Šκ³  μΈν„°νŽ˜μ΄μŠ€μ— userIdκ³Ό date만 μΆ”κ°€ν•΄μ„œ μ΄λ ‡κ²Œ 관리λ₯Ό ν•΄λ³ΌκΉŒ.. ν–ˆλ”λ‹ˆ ꡉμž₯히 λ§ˆμŒμ— 듀지 μ•Šμ•˜λ‹€. λͺ¨λ“  λ‚ μ§œμ™€ λͺ¨λ“  μœ μ €κ°€ κ°–κ³ μžˆλŠ” νˆ¬λ‘λ₯Ό ν•˜λ‚˜μ˜ λ°°μ—΄λ‘œ κ°–κ³ μžˆλŠ”λ‹€λŠ” μ μ΄μ—ˆλ‹€. 이건 μ—„μ²­ 큰 λ¬Έμ œλ‹€.

 

νˆ¬λ‘λ₯Ό κ°€μ Έμ˜¬λ•Œλ§ˆλ‹€ 배열을 λͺ¨λ‘ λŒλ©΄μ„œ ν•΄λ‹Ήν•˜λŠ” μœ μ €μ™€ λ‚ μ§œ 값을 κ°–κ³ μžˆλŠ” μ• λ§Œ ν•„ν„°ν•΄μ„œ κ°€μ Έμ˜€λ©΄ 될텐데, 더 κ·Όμ‚¬ν•œ 방법이 μžˆμ„ 것 κ°™μ•˜λ‹€. 무엇보닀 λͺ¨λ“  μœ μ €μ™€ λ‚ μ§œμ˜ νˆ¬λ‘λ₯Ό ν•˜λ‚˜μ˜ μƒνƒœ(μ•„ν†°)으둜 κ΄€λ¦¬ν•˜κΈ° λ•Œλ¬Έμ— 값이 λ°”λ€”λ•Œλ§ˆλ‹€ μ „λΆ€λ‹€ λ Œλ”λ§λ˜λŠ” 상황이 생길 수 μžˆλ‹€.

 

 

λ¦¬μ•‘νŠΈ 쿼리λ₯Ό μ‚¬μš©ν•œ μƒν™©μ΄μ—ˆλ‹€λ©΄ μ–΄λ–»κ²Œ ν–ˆμ„κΉŒ? [userId, date]λ₯Ό μΏΌλ¦¬ν‚€λ‘œ μ‚¬μš©ν•˜μ§€ μ•Šμ•˜μ„κΉŒ. 각각을 λ³„λ„μ˜ μƒνƒœλ‘œ μΊμ‹±ν•˜κ³  κ΄€λ¦¬ν•˜λŠ”κ²Œ 더 νš¨μœ¨μ μ΄λ‹€. λ¦¬μ½”μΌμ˜ atomFamily ν•¨μˆ˜κ°€ μ œκ³΅ν•˜λŠ” κΈ°λŠ₯이닀.

 

atomFamily λ„μž…

/**
 * @type [selectedDate, selectedProfile]
 */
export type ITodoItemKey = [string, string];

export const todoState = atomFamily<ITodoItem[], ITodoItemKey>({
  key: 'todo',
  default: [],
});

atomFamily λŠ” λ™μΌν•œ ν˜•νƒœμ˜ atom을 μƒμ„±ν•΄μ£ΌλŠ” νŒ©ν† λ¦¬ ν•¨μˆ˜λ₯Ό μ œκ³΅ν•œλ‹€. 무슨 말이냐면, λ§€κ°œλ³€μˆ˜λ‘œ λ°›μ•„μ˜¨ 값에 따라 각각의 아톰을 μƒμ„±ν•˜λŠ”λ°, 그게 λͺ¨λ‘ λ˜‘κ°™μ€ μΈν„°νŽ˜μ΄μŠ€λ₯Ό κ°–λŠ” 것. λͺ¨λ“  λ‚ μ§œμ™€ μœ μ €μ˜ νˆ¬λ‘λ₯Ό ν•˜λ‚˜μ˜ λ°°μ—΄λ‘œ κ΄€λ¦¬ν•˜λŠ” 것이 μ•„λ‹Œ 각각의 μ•„ν†°μœΌλ‘œ λΆ„λ¦¬ν•˜κΈ° μœ„ν•΄ μ‚¬μš©ν•œλ‹€.

 

λ§€κ°œλ³€μˆ˜λ₯Ό λ¦¬μ•‘νŠΈ 쿼리의 쿼리킀와 λΉ„μŠ·ν•œ λ°©μ‹μœΌλ‘œ μ‚¬μš©ν•˜κ³  μ‹Άμ—ˆλ‹€. 슀트링 λ°°μ—΄μ˜ ν˜•νƒœλ‘œ μ£Όκ³  @KeyλΌλŠ” 넀이밍을 썼닀. μ§κ΄€μ μ΄μ–΄μ„œ μ’‹λ‹€.

 

selectorFamilyμ—μ„œ atomFamily μƒνƒœ κ°€μ Έμ˜€κΈ°

κΈ°μ‘΄ ν”„λ‘œμ νŠΈ κ΅¬μ‘°μ—μ„œλŠ” FeedItemList.tsxμ—μ„œ νŠΉμ • μΉ΄ν…Œκ³ λ¦¬μ˜ νˆ¬λ‘λ“€λ§Œ κ°€μ Έμ™€μ„œ λ Œλ”λ§ν•˜κ³  μžˆλ‹€. μ΄μ œλŠ” νŠΉμ • λ‚ μ§œμ˜ νŠΉμ • μœ μ €μ˜, νŠΉμ • μΉ΄ν…Œκ³ λ¦¬μ˜ νˆ¬λ‘λ“€μ„ 가져와야 ν•œλ‹€. μ›λž˜ μ‚¬μš©ν•˜λ˜ selectorFamilyκ°€ atomFamily의 값을 κ°€μ Έμ™€μ„œ 가곡할 수 μžˆλ„λ‘ μˆ˜μ •ν•΄μ£Όμ—ˆλ‹€.

 

export type ITodoItemSelectorParams = {
  todoItemKey: ITodoItemKey;
  categoryLabel: string;
};

export const todosByCategory = selectorFamily<
  ITodoItem[],
  ITodoItemSelectorParams
>({
  key: 'todoSelector',
  get:
    ({ todoItemKey, categoryLabel }: ITodoItemSelectorParams) =>
    ({ get }) =>
      get(todoState(todoItemKey)).filter(
        (todo) => todo.category.label === categoryLabel,
      ),
});

atomFamily의 νŒŒλΌλ―Έν„°μ™€ μΉ΄ν…Œκ³ λ¦¬ 라벨을 λ°›μ•„μ™€μ„œ μ‚¬μš©ν•œλ‹€.

 

μΈν„°νŽ˜μ΄μŠ€ λ§Œλ“€κΈ°

export interface ITodoItemSelectorParams {
  todoItemKey: ITodoItemKey;
  categoryLabel: string;
}

type이 μ•„λ‹ˆλΌ interface둜 λ§Œλ“€μ–΄ μ‚¬μš©ν•˜λ©΄, μ•„λž˜μ™€ 같은 였λ₯˜κ°€ λ°œμƒν•œλ‹€.

 

type으둜 λ°”κΎΈμ–΄μ£ΌκΈ°λ§Œ ν–ˆλŠ”λ° 였λ₯˜κ°€ ν•΄κ²°λ˜μ—ˆλ‹€. atomFamily param의 νƒ€μž… μ€‘μ—μ„œ {[key: string]: SerializableParam} ν˜•νƒœμ˜ μΈν„°νŽ˜μ΄μŠ€λ‘œ κ΅¬ν˜„ν•˜λ €ν–ˆλ‹€. μ €λŸ°κ±Έ index signature라고 ν•˜λŠ”λ°, νƒ€μž…λ³„μΉ­μ„ μ‚¬μš©ν•΄μ•Όλ§Œ μ‚¬μš©ν•  수 μžˆλŠ” λ“― ν•˜λ‹€.

 

μ»΄ν¬λ„ŒνŠΈμ—μ„œ μ‚¬μš©

  const selectedDate = useRecoilValue(selectedDateState);
  const selectedProfile = useRecoilValue(selectedProfileState);
  const todos = useRecoilValue(
    todosByCategory({
      todoItemKey: [selectedDate, selectedProfile],
      categoryLabel: category.label,
    }),
  );

FeedItemList μ»΄ν¬λ„ŒνŠΈμ—μ„œ μœ„μ™€ 같이 selectorFamily ν•¨μˆ˜λ₯Ό ν˜ΈμΆœν•΄ μ‚¬μš©ν•œλ‹€. date와 profile μƒνƒœ λ˜ν•œ μ „μ—­μœΌλ‘œ κ΄€λ¦¬ν•œλ‹€.

 

 

2. 달λ ₯ νˆ¬λ‘ μ•„μ΄μ½˜ 색깔 μ±„μš°κΈ°

되게 재밌게 μž‘μ—…ν–ˆλ‹€. νˆ¬λ‘λ©”μ΄νŠΈλ§Œμ˜ 아이덴티티라고 ν•  수 μžˆλŠ” κΈ°λŠ₯이닀.

μ™„λ£Œν•œ νˆ¬λ‘μ˜ 색깔이 달λ ₯에도 ν‘œμ‹œλœλ‹€. ꡬ체적으둜 μ–΄λ–€μ‹μœΌλ‘œ ν‘œμ‹œλ˜λŠ”μ§€ μ‚΄νŽ΄λ΄€μŒ.

  • μ™„λ£Œλ˜μ§€ μ•Šμ€ νˆ¬λ‘μ˜ κ°œμˆ˜κ°€ ν‘œμ‹œλœλ‹€. λͺ¨λ“  νˆ¬λ‘κ°€ μ™„λ£Œλ˜λ©΄ 체크가 ν‘œμ‹œλœλ‹€.
  • 달λ ₯의 μ•„μ΄μ½˜μ€ 동그라미 λ„€κ°œλ‘œ μ΄λ£¨μ–΄μ ΈμžˆλŠ”λ°, 각 λ™κ·ΈλΌλ―Έλ§ˆλ‹€ λ‹€λ₯Έ 색상을 넣을 수 μžˆλ‹€.
  • μ™„λ£Œλœ νˆ¬λ‘λ“€μ˜ μΉ΄ν…Œκ³ λ¦¬ μ’…λ₯˜μ— 따라 λ‹€μ–‘ν•˜κ²Œ ν‘œμ‹œλ  수 μžˆλ‹€. 
  • μΉ΄ν…Œκ³ λ¦¬κ°€ ν•œκ°€μ§€μΌλ•ŒλŠ” λ‹¨μƒ‰μœΌλ‘œ, λ‘κ°€μ§€μΌλ•ŒλŠ” λ°˜λ°˜μ”©. λ„€μ’…λ₯˜μΌλ•ŒλŠ” 동그라미 ν•˜λ‚˜μ”© ν‘œμ‹œλœλ‹€.
  • μ„Έκ°€μ§€μΌλ•ŒλŠ” 색깔 ν•˜λ‚˜κ°€ 두칸을 μ°¨μ§€ν•˜λŠ”λ°, μ™„λ£Œλœ νˆ¬λ‘ 쀑 κ°œμˆ˜κ°€ λ§Žμ€ μΉ΄ν…Œκ³ λ¦¬κ°€ 두칸이닀.

 

TodoIconSvg.tsx

interface TodoIconSvgProps {
  colors: string[];
}

const TodoIconSvg = ({ colors }: TodoIconSvgProps) => {
  let fill = [colors[0], colors[0], colors[0], colors[0]];

  switch (colors.length) {
    case 1:
      fill = [colors[0], colors[0], colors[0], colors[0]];
      break;
    case 2:
      fill = [colors[0], colors[1], colors[1], colors[0]];
      break;
    case 3:
      fill = [colors[0], colors[0], colors[1], colors[2]];
      break;
    case 4:
      fill = [colors[0], colors[1], colors[2], colors[3]];
      break;
    default:
      fill = ['#DBDDDF', '#DBDDDF', '#DBDDDF', '#DBDDDF'];
      break;
  }

  return (
    <svg
      width="21"
      height="21"
      viewBox="0 0 21 21"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
    >
      <circle cx="6.46154" cy="6.46154" r="6.46154" fill={fill[0]} fillOpacity={'0.9'}/>
      <circle cx="6.46154" cy="14.5387" r="6.46154" fill={fill[1]} fillOpacity={'0.9'}/>
      <circle cx="14.5387" cy="14.5387" r="6.46154" fill={fill[2]} fillOpacity={'0.9'}/>
      <circle cx="14.5387" cy="6.46154" r="6.46154" fill={fill[3]} fillOpacity={'0.9'}/>
    </svg>
  );
};

export default TodoIconSvg;

λ„€κ°œμ˜ μ›μœΌλ‘œ 이루어진 svg μ»΄ν¬λ„ŒνŠΈλ₯Ό ν•˜λ‚˜ λ§Œλ“€μ—ˆλ‹€.colors 배열을 μ»΄ν¬λ„ŒνŠΈ λ°–μ—μ„œ λ§Œλ“€μ–΄μ„œ 쀬음. μœ„μ—μ„œ μ–ΈκΈ‰ν–ˆλ˜ 쑰건에 맞게 색을 μ±„μ›Œμ€€λ‹€. 투λͺ…도λ₯Ό 0.9둜 ν–ˆλ”λ‹ˆ μ‹€μ œ νˆ¬λ‘λ©”μ΄νŠΈλž‘ μ™„μ „νžˆ λ˜‘κ°™λ‹€..! μ€‘μš”ν•œκ±΄ μ•„λ‹ˆμ§€λ§Œ 괜히 기뢄쒋은 포인트.

 

useTodoInfo.ts

const useTodoInfo = (date: string, userId: string) => {
  const todos = useRecoilValue(todoState([date, userId]));

  const colors = todos
    .filter((todo) => todo.isDone === true)
    .map((done) => done.category.color);
  const colorSet = new Set(getSortedArray(colors));
  const colorSetArr = Array.from(colorSet);

  const count = todos.filter((todo) => !todo.isDone).length;
  const isDone = count === 0 && todos.length !== 0;

  return { count, colorSetArr, isDone };
};

export default useTodoInfo;

달λ ₯ μ•„μ΄ν…œ λ Œλ”λ§μ— ν•„μš”ν•œ 값듀을 μ œκ³΅ν•˜λŠ” λ‘œμ§μ΄λ‹€. μ™„λ£Œν•œ νˆ¬λ‘λ“€μ˜ 색깔을 κ°€κ³΅ν•΄μ„œ λ¦¬ν„΄ν•œλ‹€. λΉˆλ„μˆ˜λ‘œ μ •λ ¬ν•œ 후에 set으둜 쀑볡을 μ œκ±°ν•˜λ„λ‘ ν–ˆλ‹€.

 

CalenderItem.tsx

interface CalenderItemProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  date: string;
  userId: string;
  isSelected: boolean;
}

const CalenderItem = ({
  date,
  userId,
  isSelected,
  ...props
}: CalenderItemProps) => {
  const { count, colorSetArr, isDone } = useTodoInfo(date, userId);
  return (
    <>
      <button {...props}>
        <span className="count">{count !== 0 && count}</span>
        <TodoIconSvg colors={colorSetArr} />
        {isDone && <CheckIcon className="check" />}
      </button>
      <span className="date">{dayjs(date).date()}</span>
    </>
  );
};

μ΄λ ‡κ²Œ UIμ»΄ν¬λ„ŒνŠΈλ‘œ λΆˆλŸ¬μ™€ κ°€λ…μ„±μžˆκ²Œ ν‘œν˜„ν•  수 μžˆλ‹€.

 

λ‚˜ 살짝 μ²œμž°λ“―γ…‹