React 딜리북스 개발일지

인턴 기간 동안 개발했던 프로젝트에 대한 소개와 사용했던 기술을 공유합니다.

박세진
2021.08.17

인턴 기간 동안 도서 검색과 읽은 도서를 관리하기 위한 서비스를 만들었습니다. 보통 우리가 읽을 책을 고를 때는 직접 사이트를 돌아다니면서 책과 후기를 찾아보아야 합니다. 또한, 후기를 메모하려면 블로그나 메모 앱 등 다른 서비스를 사용해야 합니다. 이러한 불편함을 개선해 보고자 딜리북스라는 서비스를 만들었습니다.

서비스 소개

딜리북스는 도서 추천과 평가, 다이어리를 한 곳에서 관리할 수 있는 웹 서비스입니다. 딜리북스의 기능은 다음과 같습니다.

도서 추천

사용자가 입력한 데이터를 바탕으로 최근 사용자들이 많이 검색한 책, 평점이 높은 책 등으로 카테고리화하여 추천합니다.

책 추천 사진

도서 상세 정보 및 평점

책에 대한 상세정보를 확인하고, 평점을 남길 수 있습니다. 평점으로 점수와 한줄평을 작성할 수 있고, 해시태그 기능이 있어 이를 카테고리화할 수 있습니다.

상세정보

도서 검색 및 베스트셀러

원하는 책을 검색할 수 있고, 이달의 베스트 셀러를 확인할 수 있습니다.

검색

베스트셀러

다이어리

책에 대한 기록을 위해 다이어리 기능을 제공합니다. 책을 읽으면서 기억에 남는 문구나 간단한 메모를 기록할 수 있습니다. 책을 읽은 기간과 시간, 페이지를 입력하여 이를 통계로 확인할 수 있습니다.

다이어리 캘린더

메모

시스템 아키텍처

기술 스택

  • 웹 크롤링
    • Python
    • BeautifulSoup4
  • 프론트엔드
    • React
    • Redux / Redux-Thunk
    • Tailwind CSS
  • 백엔드
    • Django RestFramework
    • SQLite
    • rest_auth, all_auth

웹 크롤링

프로젝트를 진행하며 어려웠던 점 중 하나는 도서 정보에 대한 데이터를 구하는 것이었습니다. 여러 사이트에서 제공하는 open API를 사용하기 위해서는 절차와 사용 조건이 까다로웠고, 승인받는 데에도 오래 걸렸기 때문에 사용할 수 없었습니다. 그래서 대안으로 BeautifulSoup으로 도서 정보를 크롤링하여 수집했습니다. 상업적으로 이용하지 않기 때문에 웹 크롤링을 해도 문제가 없을 것이라 판단했습니다. 만약 상업적이거나 공개적으로 서비스를 제공할 경우, 크롤링이 아닌 open Api나 다른 방법으로 데이터를 수집해야 합니다.

프론트엔드

프론트엔드 프레임워크로 React를 사용했습니다. Vue를 주로 써 왔기 때문에 React의 사용은 새로운 도전이었습니다. 프로젝트를 진행하면서 느꼈던 Vue와 React의 차이는 아래에 정리했습니다.

백엔드

백엔드는 Django를 활용했습니다. 백엔드의 비중도 컸는데, Spring에 대한 경험이 조금 있었지만 빠른 시간에 많은 기능을 개발해야 했기 때문에 사용에 좀 더 능숙했던 Django를 활용하여 개발했습니다.

ERD

프로젝트에서 구현한 데이터베이스입니다.

ERD

책에 대한 좋아요 기능은 N:M 관계로 지정했습니다. 사용자는 여러 책을 좋아할 수 있고 책은 여러 사용자가 좋아할 수 있기 때문입니다. 반면에, 사용자 한 명과 매칭되는 평가 메모 등은 1:N관계로 지정했습니다.

해시태그의 경우는 특별했습니다. 해시태그를 사용하는 평가 테이블과 사용자 테이블은 N:M 관계입니다. 하지만 평가 테이블의 해시태그는 여러 사용자가 같은 태그를 사용할 수 있습니다. 이름이 같은 태그는 DB에 하나만 등록해야 그 태그에 관련된 글을 찾는 기능을 활용할 수 있기 때문입니다. 따라서 해시태그에 대한 테이블을 따로 만들고 이를 평가 테이블과 N:M 관계로 설정했습니다.

사용 기술

Vue vs. React

렌더링

React와 Vue는 모두 Virtual DOM을 활용합니다. 데이터 변경이나 페이지가 로드될 때, 실제 DOM을 직접 건드리거나 활용하는 것이 아니라 사본인 Virtual DOM에서 비교 분석하고 가상 DOM에 반영합니다. 이후 렌더링합니다. 직접 DOM을 건드리는 방식에 비해 효율적입니다. 하지만 React와 Vue의 Virtual DOM에 대해 자세히 들여다보면 다르게 동작하는 것을 알 수 있습니다.

React는 컴포넌트의 데이터가 변경되면 해당 컴포넌트를 루트로 설정하고 하위 컴포넌트를 다시 렌더링합니다. 하위 컴포넌트의 불필요한 렌더링으로 인해 속도가 저하됩니다. 반면에 Vue는 컴포넌트의 종속성을 확인합니다. 이는 렌더링 중에 자동으로 추적되기 때문에 실제 어느 컴포넌트를 재 렌더링 해야 할지 정확히 알고 있습니다. 따라서 변경된 부분만을 재 렌더링합니다.

또한, React는 동적 요소정적 요소 상관없이 모든 노드를 추적합니다. 하지만 Vue는 템플릿의 동적 요소만을 살펴봅니다. 렌더링 성능 측면에서는 Vue가 더 효율적이라고 할 수 있습니다.

데이터 바인딩

일반적으로 Vue는 양방향 바인딩을 지원하고, React는 단방향 바인딩만 가능합니다.

<template>
  <input v-model="message">
  <p>
    {{ message }}
  </p>
</template>

<script>
export default {
  data() {
    return {
      message: '',  
    };
  },
}
</script>

위 코드는 Vue입니다. Input에 text를 입력하면 message의 값이 바뀌고 아래의 message도 바뀐 데이터로 출력됩니다. 즉, jsview 모두에서 데이터가 일치합니다. 데이터 변경에 대해 따로 처리하지 않아도 되어서 편리합니다. 하지만 데이터가 변화할 때마다 다시 렌더링 되거나 데이터가 바뀌므로 성능이 감소할 수 있습니다. 또한, 자식 요소에서 부모 요소의 데이터를 변경하여 의도치 않은 문제가 발생할 수 있습니다.

function Container() {
  const [message, setMessage] = useState();

  return (
  	<>
      <input
      	defaultValue={message}
        onChange={() => setMessage(event.target)}
      >
      <p>
        { message }
      </p>
    </>
  );
}

React는 단방향 바인딩만 지원하기 때문에 바로 변경할 수 없습니다. 데이터의 흐름이 js에서 View로만 이루어지기 때문입니다. js에서 state가 변경되면 view에서는 반영되지만 view에서 값을 변화한다고 해서 state에 바로 반영되지 않습니다. 이를 위해 event를 호출해서 state를 변경하고 다시 렌더링하는 작업을 해야 합니다. 이 방법은 불필요한 렌더링이나 작업을 줄이고 의도한 대로만 변경할 수 있기 때문에 성능 관리, 유지보수에 용이합니다. 또한, 일관된 데이더 관리 로직을 갖습니다. 하지만 데이터 변경과 화면 업데이트를 해야 할 때 직접 코드를 작성해야 하는 번거로움이 있습니다.

컴포넌트 분할에 용이

React와 Vue 모두 컴포넌트를 작게 만들 수 있습니다. 하지만 React가 더 직관적이고 간결합니다.

Vue는 컴포넌트 분할에 있어 두가지 방법이 있습니다. 첫 번째로는 새 파일로 만들어서 분리하는 방법입니다. Vue의 경우엔 html, script, css가 한 파일에 들어가 있기 때문에 재활용되지 않는 컴포넌트를 위해 새 파일을 만드는 것이 부담스럽습니다.

다른 방법으로는 template을 문자열로 정의해서 사용하는 방법입니다. 이 방법의 경우 문자열로 표현하기 때문에 많은 정보를 잃을 수 있습니다. 가독성도 좋지 않고, 컴포넌트를 작성하는 코드의 일관성도 없어집니다.

반면에, React는 컴포넌트 정의 방법(SFC, Class)가 비슷하여 일관성이 있습니다. 또한 무상태 함수 컴포넌트를 만들 수 있어 가독성이 좋고 문자열로 표현하지 않기 때문에 정보 유실의 걱정도 없습니다. 따라서, 컴포넌트를 잘게 나누어 조합하는데 용이합니다.

상태 관리 및 라우팅

React와 Vue 모두 상태 관리라우팅을 지원합니다. 하지만 지원하는 주체에 있어 차이가 있습니다. Vue의 경우 Vuex, Vue-router를 공식적으로 지원합니다. 하지만 React는 사용자들이 만든 라이브러리를 활용해야 합니다. React는 방향이나 사고방식을 강요하지 않고 커뮤니티에 결정을 맡깁니다. 이 때문에 React는 자율성이 높고 생태계가 풍부하다는 특징이 있습니다.

반면에 Vue는 공식적으로 라이브러리를 지원하고 버전도 관리하여 최신 상태를 유지합니다. 사용자가 개발에만 전념할 수 있도록 도와줍니다. 반대로 커뮤니티의 발달을 저해시키는 원인이 됩니다.

TypeScipt 지원

React는 TypeScript와 호환이 좋습니다. 거의 모든 부분에서 타입을 정확하게 지정할 수 있기 때문에 추론이 가능하고 오류를 사전에 방지할 수 있습니다. 즉, TypeScript의 장점을 제대로 활용할 수 있습니다.

Vue도 TypeScript를 지원합니다. 하지만 TypeScript는 computed나 methods를 추론하기 어렵습니다. 또한, props의 경우 annotating을 사용해야 합니다. Vue3가 출시되면서 이 부분이 많이 개선됐습니다.

Tailwind CSS

CSS Framework로 Tailwind CSS를 선택했습니다. Tailwind CSS는 최근 많은 인기를 얻고 있는 CSS Framework입니다. Bootstrap과 비슷한 문법을 가지면서도 사용자가 직접 유틸리티를 만들 수 있어 디자인 시스템을 만드는 데 유용합니다.

장점

Bootstrap처럼 인라인 스타일에 적용할 수 있습니다.

// ~.tsx
<div
  className="mx-2 py-2 align-top text-center w-32 max-w-lg h-20 md:h-44 lg:h-52 relative;"
>
  ...
</div>

class에 직접 스타일을 적용할 수 있어 클래스 명을 따로 고민하지 않아도 됩니다. 또한, HTML과 CSS를 따로 관리할 필요가 없어 편리하고 개발 속도도 빨라질 수 있습니다.

디자인이 정해져 있지 않아 사용자가 직접 정의할 수 있습니다.

Bootstrap과 같은 대부분의 CSS Framework는 Component기반이기 때문에 사용하기엔 편리합니다. 하지만 커스터마이징 하여 사용하는 데 많은 불편함이 따릅니다.

반면에 Tailwind CSS는 Utility-first 기반입니다. 불필요한 기능을 빼고 low lovel의 기능을 제공하기 때문에 가볍습니다. 또한, 사용자가 직접 고유의 스타일을 적용하여 UI를 만들고 테마 등도 쉽게 적용할 수 있습니다. 디자인 시스템을 만들고 활용하기 유리합니다.

// Example `tailwind.config.js` file
const colors = require('tailwindcss/colors')

module.exports = {
  theme: {
    colors: {
      transparent: 'transparent',
      current: 'currentColor',
      black: colors.black,
      white: colors.white,
      gray: colors.trueGray,

    },
    fontFamily: {
      sans: ['Graphik', 'sans-serif'],
      serif: ['Merriweather', 'serif'],
    },
    screens: {
      'sm': '640px',
      'md': '768px',
      'lg': '1024px',
      'xl': '1280px',
      '2xl': '1440px',
    },
    extend: {
      spacing: {
        '1': '8px',
        '2': '12px',
        '3': '16px',
      },
      borderRadius: {
        '4xl': '2rem',
      }
    }
  },
  variants: {
    extend: {
      backgroundColor: ['active'],
      textColor: ['visited'],
    }
  }
}

단점

사용자가 직접 정의하는 방식이기 때문에 초보자가 사용하기 쉽지 않습니다.

문서를 보면서 작업하기 때문에 번거롭습니다.

Tailwind css는 고유의 css style 지정 방법이 존재합니다.

// 기존 css
.container {
  widthh: 75%;
}

// tailwind css
.container {
  @apply w-3/4;
}

위와 같이 기존 style 지정 방법과 다르기 때문에 공식 문서에서 해당하는 스타일을 찾아가면서 작성해야하는 번거로움이 있습니다. 익숙해진다면 속도가 훨씬 빨라지겠지만 그전까지는 오히려 속도가 느릴수 있습니다.

Redux + Redux-Thunk

상태 관리 라이브러리로 Redux와 middleware 구성을 위한 Redux-thunk를 사용했습니다. 리덕스는 대규모 프로젝트에 유리한 방식입니다. 하지만 소규모 프로젝트에서도 유용하게 사용할 수 있습니다. 페이지를 여러 컴포넌트로 나누고 같은 데이터를 사용하기 위해서는 부모에서 props를 전달받아야 합니다. 만약 컴포넌트 간 depth가 깊어진다면 데이터를 쓰지 않는 중간 컴포넌트도 props를 받고, 다시 하위로 보내야 하는 번거로움이 있습니다. 만약 수정해야 할 일이 생긴다면 모두 바꿔야 하는 끔찍한 일이 발생할 것입니다.

Redux를 사용하면 중앙 스토리지에서 상태를 관리하기 때문에 컴포넌트 간 의존성을 줄일 수 있습니다. 또한, 상태가 변하면 해당 부분만 다시 렌더링 되어 성능 향상에 많은 도움이 됩니다. 추가로 middleware를 같이 사용하면 Redux의 활용 가치를 높일 수 있습니다. 상태 변화를 하기 전에 작업을 수행하여 상태에 반영할 수 있습니다. 그리고 view와 business 로직을 분리할 수 있어 역할을 분명히하고 파일 크기를 줄일 수 있습니다.

middleware는 대표적으로 Redux-thunkRedux-saga가 있습니다.

thunk

  • 동작 과정이 단순하여 Redux를 처음 사용하는 초보자들이 사용하기 쉽습니다.
  • 액션에 객체를 반환하는 생성자와 함수를 반환하는 생성자를 같이 관리하므로 구조가 복잡해집니다.
  • 기존에는 액션이 객체를 생성해서 반환했으나, 함수도 반환할 수 있게 되어 역할이 모호해집니다.

saga

  • 액션 생성자는 객체만을 반환하기 때문에 액션의 역할이 분명해집니다.
    • 액션의 순수성을 보장합니다.
    • 제너레이터 사용으로 별도의 스레드를 fork 하는 것과 같은 효과가 있습니다(실제로 별도 스레드가 생성되진 않습니다).
  • 요청에 대한 제어를 할 수 있습니다.
    • 비동기 요청을 할 때, 기존 요청을 취소할 수 있습니다.
    • API 요청에 실패했을 때 재요청 작업을 할 수 있습니다.
      • 응답에 대한 처리가 가능합니다.
  • 액션에 따라 다른 작업을 처리하도록 나눌 수 있습니다.
    • thunk는 직접 액션 생성자를 만들어서 처리한다는 점에서 다릅니다.
  • Generator와 클로저 패턴 사용으로 초기 진입 장벽이 높습니다.

프로젝트의 규모가 작거나 초기에는 thunk를 사용해도 괜찮지만, 규모가 커진 이후에는 saga가 더 좋은 방법이 될 수 있습니다. saga는 하나의 프로세스를 더 작은 프로세스로 나누고 이에 대한 워크플로우를 지휘하여 오류 관리에 유리하기 때문입니다. 정답이라곤 할 수 없지만, 프로젝트의 규모와 필요한 기능을 따져보고 더 적합한 방법을 선택하면 됩니다.

마치며

익숙하지 않은 기술을 사용하다 보니 해당 기술의 장점을 살리기 어려웠고, 기획 단계에서 디자인을 예쁘게 만들기 어려웠습니다. 하지만 매주 팀원분들께 프로젝트 진행 상황과 코드 리뷰를 받으면서 놓쳤던 부분이나 몰랐던 부분을 알 수 있었습니다. 피드백을 바탕으로 디자인도 계속 수정해가면서 보완했고, 사용자의 요구사항과 피드백을 통해 개선해 나가는 것의 중요성을 배울 수 있었습니다. 이상으로 인턴 기간동안 진행했던 프로젝트 경험 공유를 마치겠습니다. 끝까지 읽어주셔서 감사합니다.

박세진

딜리셔스 프론트엔드 개발자

"고객을 위한 서비스를 만들고 싶습니다."