logo

프로젝트 폴더 구조의 변화, 바꾼 이유, 그리고 느낀 점

작성 2020년 9월 23일

업데이트 2020년 9월 23일

배리의 에디터는 아주 작은 단위부터 고민해 본 나의 첫 프로젝트이다. 시작하려고 보니 첫 컴포넌트를 어떤 폴더의 어떤 파일명으로 만들어야 할지가 고민이었다. 그때 조언을 구하며 container 컴포넌트와 presentational 컴포넌트로 구분하는 방법이 있다는 것을 알았다. 두 가지 개념의 특징을 정리해 보면 다음과 같다.

container와 presentational 컴포넌트 비교

항목containerpresentational
데이터직접 처리하여 자식에게 전달부모에게서 전달받은 데이터를 표시만 함
구성데이터 처리 로직 및 하위 컴포넌트를 감싸기마크업 및 스타일로 이루어진 UI
상태관리statefulstateless
별칭smart componentdumb component

예시로 이해하기

개념만 보기보다는 실제로 이 방식으로 컴포넌트를 만들어 보면 이해하기가 더 쉽다. 예를 들어 특정 나라의 실시간 뉴스 데이터를 받아와 표시해 주는 간단한 앱을 만들어 본다고 가정해 보자. 이 앱에서 container 는 뉴스 데이터를 받아오는 컴포넌트, presentational 은 그 데이터를 전달 받아서 표시만 해주는 컴포넌트라고 구분할 수 있다. 먼저 데이터를 받아오는 NewsList 라는 이름의 컴포넌트를 다음과 같이 작성한다.

// NewsList.js
import React, { Component } from 'react';
import NewsItem from './NewsItem';
import axios from 'axios';

class NewsList extends Component {
  state = {
    articles: null,
    loading: false,
  };

  loadData = async () => {
    try {
      this.setState({
        loading: true,
      });
      const { category } = this.props;
      const query = category === 'all' ? '' : `&category=${category}`;
      const response = await axios.get(
        `https://newsapi.org/v2/top-headlines?country=kr${query}&apiKey=96ad711f82654d3cb0c55d9f39f6d514`,
      );
      this.setState({
        articles: response.data.articles,
      });
    } catch (e) {
      console.error(e);
    }

    this.setState({
      loading: false,
    });
  };

  componentDidMount() {
    this.loadData();
  }

  componentDidUpdate(prevProps, prevState) {
    if (this.props.category !== prevProps.category) {
      this.loadData();
    }
  }

  render() {
    const { articles, loading } = this.state;

    if (loading || !articles) {
      return (
        <div>
          <p>로딩중입니다...</p>
        </div>
      );
    }
    return (
      <div>
        {articles.map((article) => (
          <NewsItem key={article.url} article={article} />
        ))}
      </div>
    );
  }
}

export default NewsList;

NewsList 컴포넌트에서는 데이터를 받아와서 자식에게 전달해 주고, 또 이에 따른 로딩 상태를 변화시켜 주는 역할만 하고 있다. 받아온 데이터 어떻게 표시할지는 자식인 NewsItem 에 위임하고 있음을 알 수 있다. articles 데이터가 담긴 배열을 map 하여 개별 article 이라는 이름의 prop 으로 전달되는 부분이다. 그렇다면 이 데이터를 받아와 표시해 줄 NewsItem 컴포넌트를 만들어 보자.

// NewsItem.js
import React, { Component } from 'react';
import styled from 'styled-components';

const NewsItemBlock = styled.div`
  margin-bottom: 2rem;
  overflow: hidden;

  .thumbnail {
    margin-right: 1rem;
    float: left;
    width: 160px;
    height: 100px;
    background-image: url(${(props) => props.urlToImage});
    background-position: center;
    background-size: cover;
    background-repeat: no-repeat;
  }

  .contents {
    .title {
      margin-top: 0.5rem;
      margin-bottom: 1rem;

      a {
        color: inherit;
        text-decoration: none;
      }
    }

    .description {
      line-height: 1.3;
    }
  }
`;

class NewItem extends Component {
  render() {
    const { title, description, url, urlToImage } = this.props.article;

    return (
      <NewsItemBlock urlToImage={urlToImage} color="blue">
        {urlToImage && <div className="thumbnail" />}
        <div className="content">
          <h3 className="title">
            <a href={url}>{title}</a>
          </h3>
          <div className="description">{description}</div>
        </div>
      </NewsItemBlock>
    );
  }
}

export default NewsItem;

부모에게서 전달된 개별 뉴스 데이터를 this.props.article 으로 받아와 필요한 내용만 표시해 주고 있다. 또한 presentational 컴포넌트에서는 스타일 설정도 상세하게 되어 있음을 볼 수 있다. 이것이 stateless하고 dumb한 presentational 컴포넌트의 특징이다.

처음 시작은 이랬다

이제 containerpresentational 의 차이를 알았으니, 프로젝트를 시작해 볼까? 간단하게 다음과 같은 폴더 구조를 잡고, presentational 에 속하는 UI 컴포넌트 폴더를 components 로 이름지었다.

|— vary-editor
  |— src
    |— assets
    |— components
    |— container
  |— .babel.config.js
  |— package.json
  |— tsconfig.json
  |— webpack.config.js
  |— yarn.lock

초심을 잃어가는 프로젝트의 폴더 구조 뜯어고치기

그렇게 당찬 포부로 시작된 에디터 프로젝트가 계속되고, 구현되는 컴포넌트가 늘어나며, 처음 맞닥뜨리는 개발 과제의 여러 난관들을 헤쳐 나갈수록 상황은 악화되었다. containerpresentational 컴포넌트의 경계는 점점 모호해졌고 더이상 폴더 구조를 통해 프로젝트의 구성을 예상할 수 있기는 커녕, 처음 이 구조를 만든 사람(나)이 containerpresentational 의 개념을 이해는 하고 있는 것일까? 라는 의문이 들게 되었다. 이 폐해는 협업을 할 때 드러난다. 협업시 새로운 컴포넌트를 추가해야 할 경우 어떤 기준으로 어디에 추가해야 할지가 명확하지 않기 때문에 혼란을 초래하고 이로 인해 폴더의 구분은 더욱 모호해진다.

또한 이맘때까지 프로젝트를 진행하면서 느낀 점은, containerpresentational 이라는 구분 방법을 고수한다는 가정 하에 이러한 문제를 해결하기보다는 다른 방법을 생각해 보는 게 좋겠다는 것이었다. 그 이유는 다음과 같았다.

  1. containerpresentational 라는 두 가지 기준으로 모든 컴포넌트를 구분하기에는 다른 경우의 수들이 많다.
  2. presentational 컴포넌트에서 직접 리덕스 글로벌 스토어에 접근하거나 상태 관리를 하고 싶은 경우가 생긴다.
  3. 다소 포괄적인 의미의 containerpresentational 이라는 이름 대신 명확한 이름을 사용하고 싶다.

그러나 에디터가 완성되지 않은 시점에서 구조를 전부 바꾸는 것은 적절하지 않아 보였고, 에디터는 물론 앱이 정식으로 출시되고 난 후에 시간을 내어 바꾸어보기로 결심하였다. 그렇게 기다림 끝에 바뀐 폴더 구조는 다음과 같다.

|— vary-editor
  |— src
    |— assets
    |— components
      |— buttons
      |— editors
      |— features
      |— groups
      |— modal
      |— plugin
      |— rich-editors
      |— tabs
      |— wrappers
    |— executors
    |— renderers
    |— store
  |— .babel.config.js
  |— package.json
  |— tsconfig.json
  |— webpack.config.js
  |— yarn.lock

훨씬 구분이 명확해졌다. 우선 components 폴더에는 컴포넌트들이 기능에 따라서 구분되어 있고, executors 폴더는 앱과 데이터를 주고받기 위한 로직, renderers 폴더는 에디터에서 편집한 내용을 다양한 형태로 렌더링하는 로직, store 는 리덕스 글로벌 스토어 관련 로직을 포함하고 있다. 마지막으로 결국 필요가 없어진 container 폴더는 사라졌다.

시행착오는 교훈을 남긴다

containercomponents 로 컴포넌트를 구분하는 방식은 현재로서는 잘 쓰이지 않는 방식이라고 한다. 내가 겪었던 문제와 같이, 프로젝트 규모가 커질수록 이 방식으로 모든 컴포넌트를 규정하기는 어렵기 때문일까 싶다. 리액트에서 상태를 관리하고 전달하는 매커니즘에 대해 익히기 위한 목적으로 간단한 예제를 만들어보는 경우라면 이 방식을 실습해 보는 것이 도움이 된다. 하지만 실제 프로젝트를 진행할 때는 프로젝트에 맞는 폴더 구조를 고민해 보고 적용하는 것이 좋겠다.