wanted preonboarding internship 1주차 - Todolist

박준수·2023년 4월 19일
0
post-thumbnail

프로젝트의 주제

  • 원티드에서 주관하는 프리온보딩 인턴십에는 사전 과제 todo-list가 있었다.
  • 인턴십이 시작하고 첫번째 주차의 과제는 이 todo-list를 각자 리팩토링하고 코드를 다 같이 합쳐 best practice를 선출하는 것이었다.

프로젝트의 요구사항

  • 과제를 수행하고 팀원들과 토론을 통해 Best Practice를 도출
  • 동료학습, 팀으로 일하는 법에 익숙해지는 것
  • 과제를 대하는 태도를 연습
  • 로그인/회원가입 기능 구현
  • Todo Create / Read
  • Update / Delete

프로젝트에서 도입된 주요 기술

git flow 전략 및 코드 리뷰

왜?

  • 우리는 앞으로 4주간 같이 협업하며 프로젝트를 진행해 나간다.
  • 이때 처음에 모두가 git 사용 전략에 대해 협의하면 통일성을 증가시키고 git과 관련하여 불필요한 혼선을 줄일 수 있다.
  • Git을 통해 History를 확인하기 쉬워진다.
  • 코드 리뷰방식을 정해두면 모두가 소통하는데 좀 더 편리하게 할 수 있고 하나의 tool(github)에 개발관련 history를 정리할 수 있다.

git issue관리하기

  • 기존의 문제점

    • issue제목만 보고 어떤 이슈인지 파악하기 힘듬
    • issue를 분할하여 EPIC과 feature, refactor들로 나누지 않았다.
  • 개선 이후

    (해당 사진은 2주차 과제의 이슈이지만 원활한 설명을 위해 여기에 넣었다.)

    • EPIC이슈와 feature이슈를 따로 나눔으로써 PR시 좀더 세부적이고 명확한 PR이 가능하다.
    • 이번 프로젝트에서 어떤 issue들이 있었는지 정리가 되므로 Git의 History가 상세해진다.

Commit Convention & Commit Rules

  • 기존의 문제점

    (놀랍게도 커밋 하나만으로 issue하나를 끝냈다.)

    (커밋메시지가 아예 없다.)

    • 기존의 나는 커밋 컨벤션과 커밋메세지에서 issue number를 적는 것은 알고 있었으나
    • 커밋하나의 내용이 매우 거대했고 커밋메시지에 추가로 Description이 들어가지 않았었다.
    • 그로 인해 git을 통해 코드리뷰를 할 때 커밋설명이 없어 코드를 보는사람이 진입장벽과 불편함을 느끼고
    • 커밋하나의 내용이 매우 컷기에 커밋단위 또한 커졌고 이는 커밋단위로 코드리뷰를 하기 불편해지는 상황을 초래했다.
  • 개선 이후

    • 커밋컨벤션에 맞게 커밋을 진행했고
    • 커밋을 하는 단위를 가능한 잘게 쪼갰다.
    • 또한 상세한 설명을 붙여 커밋을 보는 사람이 코드를 읽기 전 미리 내용을 파악할 수 있게 했다.
    • 이로써 git을 이용한 협업이 조금 더 원활해지는 결과를 가질 수 있다.
    • 또한 커밋목록을 보며 코드리뷰를 함으로써 왜 커밋을 잘게 쪼개야 하는지, 그리고 코드리뷰 방법에 대해 알 수 있었다.

eslint, prettier, .husky

왜?

  • 작업자마다 각자 다른 코딩 스타일을 가지고 있고, 그것이 코드에 드러난다면 이 프로젝트를 제 3자가 읽기도 어려워지며, 팀원들끼리도 다른 팀원들이 작성한 코드를 읽고 이해하기가 힘들어진다.
  • 이러한 요소들은 결국 비효율을 유발하게되고 이를 극복하기 위해서 팀으로 작업을 할 때는 여러 작업자들의 코딩 스타일을 일치시키기 위한 Lintter와 Code Formatter를 사용하는 것이 좋다.
  • 또한 .vscode 설정을 통해 각 팀원의 vscode설정을 통일 시킬 수 있다.
  • 마지막으로 이런 lint와 formatter를 도입해도 작업자가 사용을 안하면 효과가 없기에 자동화를 통해 신경쓰지 않아도 자동으로 적용이 되게 하고 특정 상황에서 강제로 적용이 되게 하면 좋다.

.husky

  • 기존의 문제점

    • githook을 사용하지 않아 다른 팀원이 lint나 formatter를 하지 않아도 진행이 되었다.
    • 개인이 매번 확인해서 실행하는 것은 실수가 발생할 여지가 있으며 강제성이 없다.
  • 개선 이후

    • 처음에 npm install husky --save-dev 를 통해 husky를 devDependencies에 설치한다.

    • 이후 npx husky install 을 통해 .husky 폴더를 만들어 준다.

    • package.json에서 script를 설정해준다.

      • postinstall은 npm install후에 자동으로 husky install 이 될 수 있도록 하는 설정이다.

        // package.json
        
        {
          "scripts": {
            "postinstall": "husky install",
        		"format": "prettier --cache --write .",
        		"lint": "eslint --cache .",
          },
        }
    • add pre-commit, pre-push hook

      1. npx husky add .husky/pre-commit "npm run format"
      2. npx husky add .husky/pre-push "npm run lint"
    • 이렇게 하면

      (파일 내부에 명령어가 생긴다.)

    • 이로써 git clone하고 npm install한 사람들은 자동으로 git hook설정이 끝나게되고 commit 혹은 push를 할때마다 모든 팀원들이 같은 설정으로 코드가 정리되게 된다.

심화

  • 실제로 이 husky를 적용하면 생각과는 다르게 commit 이후에 formmating이 진행되는 것을 볼 수 있다.
  • 그렇다면 이 문제를 어떻게 해결하면 좋을까?
  • 우선 husky내에 여러 git command를 조합해서 다시 커밋이 돌아가게 하는 방법이 있다.
  • 하지만, 저런 커맨드를 조합하는게 다소 복잡하고, 또 여러가지 엣지케이스들까지 고려하다보면 복잡성이 올라간다..
  • 그래서 현업에서는 lint-staged라는 라이브러리를 쓴다고 한다.
  • 이 방법을 사용하면 commit 전에 formaating과 linting을 다 사용할 수 있다.
    // package.json
    "lint-staged": {
        "*.{js,jsx,ts,tsx}": [
          "prettier --cache --write",
          "eslint --cache --fix --max-warnings=0"
    	  ]
    }
    // .husky/pre-commit
    
    #!/usr/bin/env sh
    . "$(dirname -- "$0")/_/husky.sh"
    
    npx lint-staged

컴포넌트와 관심사의 분리 및 아키텍쳐 설계

왜?

  • 컴포넌트는 왜 분리되어야 할까?
    • 리액트에서 컴포넌트는 다양한 역할을 한다.
    • 어떤 컴포넌트는 UI를 표현하기도 하고
    • 어떤 컴포넌트는 동작하는 로직을 담고 있기도 한다.
    • 만약 하나의 컴포넌트가 여러 UI를 담당하거나 여러 동작을 나타낸다면 어떻게 될까?
    • 재사용할 수 없게 되고, 나중에 다시 봤을 때 어떤 컴포넌트인지 이해하기 힘들어져서 가독성과 유지보수성이 떨어지게 된다.
    • 따라서 컴포넌트는 재사용할 수 있는 최소 UI 단위이어야 한다.
    • 그럼 어떤 기준을 가지고 컴포넌트를 분리해야할까?
  • 기준은 관심사
    • 컴포넌트의 재사용성과 가독성, 유지보수성을 올리기 위해 관심사라는 기준으로 컴포넌트를 분리해야한다.
    • “관심사"를 간단히 말하면 하나의 모듈이 수행하고자 하는 목적이다, 여기서 모듈이란 함수, 클래스 등의 단위로 해석할 수 있다.
    • 따라서, 관심사의 분리란 각 모듈들이 한번에 여러 관심사를 처리하려고 하지 않고, 하나의 관심사만 처리하도록 분리하는 것을 의미한다.
    • 관심사를 분리하면 하나의 모듈은 하나의 목적만 가지게 된다.
    • 하나의 목적만 가지게 된다는 말을 조금 다르게 해석해보면, 이제 이 코드가 수정될 이유는 한가지만 존재하게 된다는 의미이다.
    • 이러면 자연스럽게 재사용하기 편해지고 읽기 쉬우며, 유지보수하기 좋아진다.
  • 아키텍쳐는 왜 중요할까?

    아키텍처란 구조화 된 옷장과 비슷한 겁니다. 처음 개발 할 때에는 규칙없이 그냥 코드를 만들다 보면 덩치가 커지는 순간 불편함이 생기고 정리가 안 되는 시점이 생깁니다. 그러니 처음부터 특정한 규칙을 만들어서 개발을 하는게 좋다는 것을 알게 되고 규칙을 하나씩 만들어가며 개발을 하다보면 이것이 반복이 되어 하나의 특정 패턴이 만들어집니다. 이러한 패턴들을 모두가 이해하고 따를 수 있도록 하는 구조를 아키텍쳐라고 부릅니다.

    (출처: https://velog.io/@teo/프론트엔드에서-MV-아키텍쳐란-무엇인가요#1-아키텍쳐란-무엇일까요)

    • 아키텍쳐가 없거나 이상하다면 우리는 옷을 계속해서 이상하게 놓을것이며 옷이 쌓이면 쌓일 수록 이는 걷잡을 수 없게 될 것이다.
    • 소프트웨어 관점에서 지속적으로 관리가 잘되는 코드를 위해서는 좋은 아키텍쳐가 필요하다
      는 의미이며 그러기 위해 웹에서도 좋은 아키텍쳐의 모습이 지속적으로 진화하고 있다.

컴포넌트 구조 및 폴더 구조

📦src
 ┣ 📂apis
 ┃ ┣ 📜authApi.js
 ┃ ┗ 📜todoApi.js
 ┣ 📂components
 ┃ ┗ 📂TodoList
 ┃ ┃ ┣ 📂TodoItem
 ┃ ┃ ┃ ┣ 📜index.jsx
 ┃ ┃ ┃ ┗ 📜useTodoItem.jsx
 ┃ ┃ ┗ 📜index.jsx
 ┣ 📂constants
 ┣ 📂hooks
 ┃ ┣ 📜useAuthForm.jsx
 ┃ ┣ 📜useInput.jsx
 ┃ ┗ 📜useMovePage.jsx
 ┣ 📂pages
 ┃ ┣ 📂Error
 ┃ ┣ 📂Root
 ┃ ┣ 📂SignIn
 ┃ ┣ 📂SignUp
 ┃ ┣ 📂Todo
 ┃ ┃ ┣ 📜index.jsx
 ┃ ┃ ┗ 📜useTodo.jsx
 ┃ ┗ 📜index.js
 ┣ 📂router
 ┃ ┣ 📂loaders
 ┃ ┃ ┣ 📜authLoader.js
 ┃ ┃ ┣ 📜index.js
 ┃ ┃ ┣ 📜rootLoader.js
 ┃ ┃ ┗ 📜todoLoader.js
 ┃ ┗ 📜index.js
 ┣ 📂utils
 ┃ ┣ 📜index.js
 ┃ ┣ 📜storage.js
 ┃ ┗ 📜validator.js
 ┣ 📜App.jsx
 ┗ 📜index.js
  • 우선 page를 총 4개로 나누었으며
  • 이중 signin과 signup은 따로 컴포넌트를 만들지 않고 페이지내에서 UI를 끝낸다.
    • 비즈니스 로직은 useAuthForm으로 따로 만들어 관심사 분리를 했다.
  • Todo의 경우 컴포넌트가 TodoList, TodoItem으로 분리되어지는데
    • 이는 UI적으로 재사용성을 높이기 위한 방편이다.
    • 또한 TodoItem을 따로 컴포넌트로 만듦으로써 유지보수성을 높였다.
    • TodoItem의 경우 TodoList에서만 사용되므로 TodoList의 하위에 넣어놓았다.
  • Todo의 비즈니스로직들을 따로 custom hook으로 분리했다.
    • 이 hook들은 다른 컴포넌트에서 재사용하기보다는 비즈니스로직을 분리하기 위한 용도의hook이므로 해당 컴포넌트의 폴더에 위치해 응집도를 높였다.

refetch 함수 구현

export const Todo = () => {
 ...
  const [todos, setTodos] = useState(loadedTodoData);
  const [isUpdated, setIsUpdated] = useState(false);

  const refetchTodos = async () => {
    const refetchedTodos = await getTodos();
    setTodos(refetchedTodos);
    setIsUpdated(false);
  };

  useEffect(() => {
    refetchTodos();
  }, [isUpdated]);

  const handleCreateTodo = async (e) => {
    ...
    await createTodo({ todo: todoValue });
    setIsUpdated(true);
    resetTodoInput();
  };
  • 새로운 Todo를 생성 후, 생성된 Todo를 포함한 Todo 데이터들을 새로 받아오기 위하여 재요청을 보내는 refetchTodos 함수를 구현했다.
  • isUpdated를 플래그 상태 변수로 두고, 이것을 useEffect의 dependency 배열에 넣었다.
  • 따라서 isUpdated의 상태가 변경되면 refetchTodos 함수가 다시 호출되고, 새롭게 받아온 투두 데이터를 todos 상태 변수에 저장한다.
  • 이러한 방식을 사용해 서버의 데이터와 클라이언트의 데이터를 동기화하여 화면에 최신 데이터를 보여줄 수 있다.

update기능 구현

// components/TodoItem/hook.jsx

const handleUpdateTodo = async () => {
  await updateTodo(id, { todo: todoValue, isCompleted });
  setIsUpdated(true);
  setUpdatedTodoId(id);
};

useEffect(() => {
  if (!isUpdated && updatedTodoId === id) {
    setIsEdit(false);
    setUpdatedTodoId(null);
  }
}, [isUpdated]);
  • Todo update의 로직은 다음과 같다.
  1. 사용자가 Todo를 수정하고, 제출 버튼을 클릭하면 handleUpdateTodo 함수가 호출된다.
  2. isUpdated 플래그 변수가 true로 변경되어 refetch 함수가 실행된다.
  3. 서버에서 새로운 todo를 받아 렌더링하고, isUpdated 플래그 변수를 false로 변경한다.
  4. isUpdated 변수가 변경되어 useEffect에 등록한 함수가 실행되고, isEdit 상태 변수를 false로 변경하여 input을 숨겨준다.
  • useEffect를 쓴 이유는 다음과 같다.
    • 만약 useEffect를 쓰지 않고 handleUpdateTodo 함수에서 바로 setIsEdit(false)를 호출한다면, 아직 서버에서 데이터를 받아 렌더링을 하기도 전에 input이 숨겨진다.

    • 즉 수정 전의 todo가 한 번 보인 후, 렌더링이 되는 것이다.
      - 이것은 사용자에게 어색한 경험을 줄 수 있다.

      어색한

      (서버와의 비동기 통신이 이루어지기 전에 input이 숨겨지는 경우)

      올바른

      (useEffect를 이용해서 서버와의 비동기 통신이 이루어지기 나서 input이 숨겨지는 경우)

router의 loader기능 활용

  • React Router Dom에서 제공하는 기능인 loader와 redirect를 활용하여 로그인 여부에 따른 리다이렉트를 처리한다.
  • 각 경로(route)마다 loader 함수를 정의할 수 있으며, 이 loader 함수는 렌더링 하기 전에 실행된다.
  • 따라서 유저에게 해당 라우트의 페이지를 보여주기 전에 작업을 수행할 수 있게 된다.
  • loader에서 반환된 데이터는 컴포넌트에서 useLoaderData훅을 통해 사용할 수 있다.
  • loader 함수는 어떤 값을 반환해야 에러가 발생하지 않으므로 이 경우 null을 반환해 준다.
    import { PATH_ROUTE } from 'constants';
    import { SignIn, SignUp, Todo, Error, Root } from 'pages';
    import { createBrowserRouter } from 'react-router-dom';
    import { authLoader, rootLoader, todoLoader } from './loaders';
    
    const routes = [
      {
        path: PATH_ROUTE.root,
        element: <Root />,
        errorElement: <Error />,
        loader: rootLoader,
      },
      {
        path: PATH_ROUTE.signIn,
        element: <SignIn />,
        errorElement: <Error />,
        loader: authLoader,
      },
      {
        path: PATH_ROUTE.signUp,
        element: <SignUp />,
        errorElement: <Error />,
        loader: authLoader,
      },
      {
        path: PATH_ROUTE.todo,
        element: <Todo />,
        errorElement: <Error />,
        loader: todoLoader,
      },
    ];
    
    export const router = createBrowserRouter(routes, {
      basename: '/pre-onboarding-9th-1-5',
    })

끝으로

  • 이번 과제를 하면서 정말 많은 것들을 배우고 좋은 인사이트들을 얻었다.
  • 아키텍쳐에 한발 더 접근했고 코드적으로 어떻게 구현해![]
    나갈지 알게 되었다.
  • custom hook을 사용해 비즈니스로직을 분리하는 이유와 그 방식에 대해 알게 되었다.
  • 프론트엔드 프로젝트의 기본인 todo-list를 통해 CRUD구현의 방식을 알게 되었다.
  • 특히 update의 경우 서버와의 통신시 어떤 방향으로 로직을 구현해야 UI적으로 좋은 지 알게 되었다.
profile
심플한 개발자를 향해 한걸음 더 (과거 블로그 : https://crablab.tistory.com/)

0개의 댓글