Skip to content

TodoList API 개발기

daeseong9388 edited this page Dec 11, 2022 · 7 revisions

상황

  • 기획을 마무리 한 후 백로그를 작성했다.
  • 컴포넌트 구현과 Todo 데이터 관리 시스템 구현을 최우선순위로 설정했다.

문제

  • 컴포넌트 구현을 위해서는 Todo 데이터 관리 시스템 구현이 선행되어야한다.
  • 백엔드 구현이 우선순위에서 밀려있어 어떤 DB를 사용해 데이터를 저장하고 동기화할 지 알 수 없다.

해결

  • Todo 데이터 관리시스템의 API를 먼저 정의해 다른 도메인의 구현이 API의 구현 정도와 독립적으로 진행될 수 있도록하자
  • 추후 DB 관련 로직이 비동기인 점과 핵심 비즈니스 로직의 복잡도를 고려해 제공하는 API를 비동기로 만들자

해결과정을 조금 더 자세하게 들여다보자!

인터페이스 정의

비즈니스 로직을 구현함에 있어서 내부적으로 필요한 helper 함수들은 실제 API로 사용되는 함수들과는 다르게 접근을 막아야한다. 따라서 인터페이스를 정의하고 private 키워드를 사용해 이 둘을 분리했다.

export interface ITodoList {
  // TEST 전용
  updateAll: (date?: Date) => Promise<ITodoList>; // test용
  getSortedRTL: (today?: Date) => Promise<PlainTodo[]>; // test용
  getTL: () => PlainTodo[]; // test용
  // READ
  getActiveTodo: () => Promise<PlainTodo>;
  getSortedList: (type: 'READY' | 'WAIT' | 'DONE', compareArr: string[]) => Promise<PlainTodo[]>;
  getSummary: () => any;
  getTodoById: (id: string) => Promise<PlainTodo | undefined>;
  // CREATE, UPDATE, DELETE
  postponeTemporally: () => Promise<ITodoList>;
  postponeDeadline: () => Promise<ITodoList>;
  postponeForToday: () => Promise<ITodoList>;
  lowerImportance: () => Promise<ITodoList>;
  setDone: () => Promise<ITodoList>;
  updateElapsedTime: (elapsedTime: number) => Promise<ITodoList>;
  add: (todo: InputTodo) => Promise<ITodoList>;
  edit: (id: string, todo: InputTodo) => Promise<ITodoList>;
  remove: (id: string) => Promise<ITodoList>;
}
import { ITodoList } from '@core/todo/todoList.interface';

const topologySort = async (
  todoList: ITodoList,
  filter?: (todo: PlainTodo) => boolean,
): Promise<Map<string, DiagramTodo>> => {
  • 실제 사용할 때는 인터페이스를 타입으로 받는다!

추가적으로, MDN처럼 API문서까지 작성해 좀 더 상세한 정보를 제공했다.

비동기

자바스크립트는 싱글 스레드 언어이다.

  • 익히들 알고 있는 사실이다. 그래서 만약 웹에서 모든 코드들이 동기적으로만 일을 하게 된다면 최상단에 있는 것부터 로딩이 시작될 것이며, 중간에 끊지도 못해 다른 명령(ex. 마우스 클릭)도 실행하지 못하게 된다.

이전에는 I/O 혹은 network 관련 로직을 처리할 때 이외에는 전부 동기적으로 구현했었다. 즉, 비즈니스 로직을 비동기로 구현해본 경험이 없었다.

굳이 I/O, network와 관련이 없는 것들까지 비동기로 작성해야할까?라는 고민에 대해 팀원들과 함께 의견을 나눴고 다음과 같은 이유로 인터페이스로 제공되는 함수 전체를 비동기적으로 구현하기로 결정했다.

  • 확장성
    • 추후 비즈니스 로직에 DB와 같은 I/O 관련 로직이 추가될 수 있다.
    • 비동기 → 동기로의 변경은 자원이 적게 소모되지만 동기 → 비동기로의 변경은 자원이 많이 소모된다. 따라서 어차피 일어날 일이기에 미리 비동기로 구현을 하는 것이 이득이다.
  • 메모리 + 가독성 vs 동시성
    • 추후 데이터가 늘어났을 때 복잡도가 있는 비즈니스 로직을 실행시킨다는 가정하에 동시성을 달성할 필요가 있다.

위에 있는 인터페이스를 보면 전부 Promise 인스턴스를 반환하는 것을 알 수 있다.

구현과 사용 예시

export interface CUD {
  add: (label: string) => Promise<TodoList>;
  delete: (id: string) => Promise<TodoList>;
  edit: (id: string, label: string) => Promise<TodoList>;
  setDone: (id: string) => Promise<TodoList>;
  toggleDone: (id: string) => Promise<TodoList>;
  clearDone: () => Promise<TodoList>;
}
export const TodoItem = ({
  todo,
  type,
  todos,
  setTodos,
}: {
  todo: PlainTodo;
  type: FilterType;
  todos: TodoList;
  setTodos: React.Dispatch<React.SetStateAction<TodoList>>;
}) => {
  
	...

  const handleToggle = () => todos.toggleDone(todo.id).then(setTodos)
  const handleDestroy = () => todos.delete(todo.id).then(setTodos)
  const handleChange = (event: any) =>
    todos.edit(todo.id, event.target.value).then(setTodos)
	
	...

	return ...
}
  • 비동기 프로그래밍이 낯설게 느껴지지만 실제로 READ를 제외한 CUD에 대해서는 애초에 비동기적인 이벤트를 처리하는 핸들러단에서 사용되기 때문에 사용하는데 큰 문제는 없다.
  • 다만, READ(fetch) 혹은 초기화 처리는 추가적인 상태와 useEffect 를 사용해야 한다.

참고. 비동기 처리

  • 콜백
    • 비동기 처리 결과를 외부에 반환하지 못함
    • 비동기 처리 결과를 상위 스코프의 변수에 할당하지 못함
    • 여러개의 비동기 처리를 하는 경우, 순서가 보장되지 않는 문제
    • 콜백함수를 다른 함수로 전달하는 순간 그 함수에 대한 제어권을 잃게되는 문제
  • Promise
    • Promise는 비동기 상황을 일급 객체로 다룬다. 즉, 값으로 다루어진다는 뜻이고 변수에 할당 혹은 함수로 전달할 수 있다.

    • Promise 가 중첩되어도 한 번의 then 을 통해 결과물을 꺼낼 수 있다.

      • 타이밍을 조절할 수 있다. resolve 가 호출 된 순간을 포착하는 것.

      • then 안에서 또 return한다면 다시 Promise 인스턴스를 반환한다(체이닝).

        Promise.resolve(1).then(console.log) 
        // 1을 출력하고 undefined를 값으로 가지고 있는 pending promise 반환
    • 요약

      • 콜백의 호출 시점은 then 으로 등록된 순서대로 실행
      • 콜백이 호출되지 않은 경우 → Promise.race
      • 콜백의 제어권을 잃어도 값만큼은 지킨다

API 구현체 개발

  • API를 먼저 정의한 후에 로직을 구현하니 다음과 같은 장점이 있었다.
    • 무엇을 구현해야하는지가 명확하다.
    • 촉박한 일정에 어떻게 구현할지 보다 어떻게든 동작하게 구현하는데 집중할 수 있다.
    • 추가로 필요한 API가 생겨도 바로 대응할 수 있다.
  • DB대신 메모리에서 동작하는 로직을 개발했다.
    • 알고리즘 구현의 난이도와 남아있는 자원을 생각했을 때, 메모리를 일종의 DB로 간주하고 로직을 개발하는 것이 타당하다고 판단했다.

미리 설계한 인터페이스를 토대로 기획서를 따라가며 내부 로직을 구현했고 최종적으로 병렬적으로 진행한 다른 작업과 병합하는 과정에서 별탈 없이 마무리할 수 있었다.

남은 고민들..

TodoList API는 어떤 역할을 하고 있는 것일까? 어떤 layer로 추상화를 해야할까?

  • 현재: 뷰(화면 선택, 사용자 입력, 서버와의 통신..) - 모델(TodoList API 혹은 추후 구현할 백엔드)
    • 프론트엔드에서 뷰와 모델은 서로 양방향이다. 뷰가 변하면 모델도 변하고 모델이 변하면 뷰도 변해야한다.
      • 이 관계를 단순화 시켜주는 것이 react의 flux 패턴
        • 무조건 store → 뷰의 단방향
        • store의 변화를 일으키는 것은 action(여러 이벤트)
      • MVVM 구조: 뷰와 모델을 바인딩 시켜주는 것.. ex) svelte
    • 뷰는 계층적(DOM은 트리구조)이고 다양하며 그에 따라 모델도 다양하다.
      • DOM 변화를 또 최소화 해야한다 → react의 필요성
  • 데이터를 변화하는 코드가 뷰 코드에 흩어져있지 않고 모델에 모여져 있다.
    • 일종의 reducer역할을 한다고 볼 수 있다.
      • 다만 reducer와 다른 점은?
        • reducer는 일종의 상태 관리 + 모델이라면 TodoList API는 모델만 있는 것이다.
        • 완전한 reducer로의 전환이 필요한 것일까에 대한 고민이 든다.
    • 모델과 뷰간의 의존성을 낮추고 있다.
    • 응집도가 높다 → 디버깅에 용이하다.
  • DB interface와의 결합 혹은 분리?
    • 결합
      • 의존하는 객체를 주입하기(의존성 주입)
        • 객체에 정보를 저장하고 있다가(생성자 혹은 init 메서드), 메소드에서 참조하기
        • 메소드에 의존하는 객체를 인자로 넘기기
        • 핵심 로직을 테스트할 때 I/O interface를 mocking을 해야하는 번거로움이 존재함
    • 분리
      • 데이터를 주입하기
        • 핵심 코드를 순수하게 유지하고 모든 I/O를 가장자리(시작 / 종료점)으로 밀어넣기
        • I/O가 중간에 들어가지 않도록 설계하는 것이 쉽지 않음(언제 I/O와 동기화를 하는가에 대한 기준 잡기가 어려움)

💊 비타500

📌 프로젝트

🐾 개발 일지

🥑 그룹활동

🌴 멘토링
🥕 데일리 스크럼
🍒 데일리 개인 회고
🐥 주간 회고
👯 발표 자료
Clone this wiki locally