IT/언어

[Vue.js] Atomic Design 베이스의 Vue 컴포넌트 설계

개발자 두더지 2023. 1. 10. 23:37
728x90

일본의 한 블로그 글을 번역한 포스트입니다. 오역 및 의역, 직역이 있을 수 있으며 틀린 내용은 지적해주시면 감사하겠습니다.

 

 

컴포넌트 설계와 Atomic Design의 관계


  Atomic Design은 원래 UI 설계를 위해 만들어진 것이다. 거대한 어플리케이션도 효율 좋게 부품으로 재이용하고, 더욱이 상세한 조절도 가능하도록 고려한 것이다.

  Atomic Design에서는 "컴포넌트"이라는 단어가 사용되고, 한 번보면 vue등 어플리케이션에도 그대로 설계로써 사용할 수 있다고 생각될 수 있다.

 그러나 UI 설계의 방법, 사상을 그대로 어플리케이션의 컴포넌트 설계에 적용하면 일부분 되지 않는 곳이 있다. 어플리케이션 개발에서는, 동일한 레이어 아웃에서도 데이터의 갱신의 타이밍이 다른 경우가 있다.

 이 데이터의 흐름이나 로직을 어디서 행할지라는 개념이 있으므로, UI 설계용으로 만들어진 Atomic Design을 그대로 어플리케이션에 사용할 수 없는 이유가 된다.

  하지만, Atomic Design의 각 단계의 관계성에 주목하면,  "데이터의 갱신이 역할상 가능한 단계"와 "레이아웃을 조정하는데에 전념하는 단계"로 나누고 있다는 것을 알게 됐다.

 

Atomic Desing의 단계와 역할

단계 내용 UI예
Atoms 이 이상 나눌 수 없는 단위 버튼, 텍스트 입력란, 라벨
Molecules Atoms를 여러개 합친 것 입력폼(라벨 + 입력란)
Organisms 1개의 명확한 기능을 가진 것 헤더
Templates 페이지의 틀을 구성하는 것 와이어 프레임
Pages 페이지에 컨텐츠가 있는 것 디자인 완성 견본

 이 표가 Atomic Design에서 제창되고 있는 5단계이다.

 잘 살펴보면 Atoms, Moleculs, Templates는 레이아웃으로 정리할 수 있고, Prganisms와 Pages는 컨텐츠 베이스로 묶을 수 있다.

 "레이아웃으로 정리"는 안에 있는 정보와 관계없이, 보여지는 것, 기능 단위로 정리된다는 것을 의미하고, "컨텐츠 베이스로 묶을 수 있는 것"은 컨텐츠(데이터) 단위로 엮을 수 있다는 것을 의미한다.

 "레이아웃에 관심이 높다." , "컨텐츠에 관심이 높다." 각각의 정리 방법을 컴포넌트 설계로 변현해보면, 다음과 같이 말할 수 있다.

  • Atoms, Molecules, Templates 는 유저 인터페이스나, 부모 컴포넌트로 받은 데이터를 어덯게 표현할지에 전념한다.
  • Organisms와 Pages는 컨텐츠에 관심이 높다. 즉, API나 store 등 데이터의 획득, 변경의 책임을 가진다.

 이것은 Redux가 권장하는 Presntational Component(표시하기 위한 컴포넌트)와 Container Component(로직 컴포넌트)에 가까운 분리 방법이다.

 그리고 원래 UI 설계로써의 Atomic Design에서도, 단계를 거스르는 것은 금지되어 있다. 자신보다 위의 단계에 있는 컴포넌트에 끼워넣는 것은 안된다. 반대로, 자신보다 낮은 단계의 읽어들이는 것은 가능하다. 예를 들어, Organisms는 동일 단계의 읽어들이기가 가능하다.

 이 룰을 붕괴시켜버리면, 레이아웃의 의존 관계도 망쳐버리게 되므로 UI설계에서는 금지로 되어 있다. Vue의 컴포넌트 설계에서도, 데이터와 컨텐츠의 의존관계를 생각하면, 단계를 거스르는 것을 금지해야한다.

 

 

컴포넌트의 크기 판별


 크기에 대해서 Atomic Design에서는 특히 Atoms, Molecules, Organisms를 어떻게 나눌지에 고민하는 것이 많을거라고 생각된다.

 Atomic Design의 각 단계를 어떻게 HTML 코딩할지에 설명하면 다음과 같다.

  • Atoms는 button, input등 기본 1개의 태그로 구성되는 것(예를 들어 option을 아들 요소로 가지는 select)
  • Moleculs는 여러 개의 태그로 최소한의 레이아웃을 구성하는 것(입력 폼과 검증 세트 등)
  • Organisms는 섹셔닝 컨텐츠

  "Organisms는 섹셔닝 컨텐츠"이란 무엇인지에 대해서 다시 의문이 들 것으로 생각되는데, 기본적으로는 아래와 같이 설명할 수 있다.

1. 표제를 가진다.

2. section, article, aside, nav으로 마크업될된다.

3. 내용에 대해서 부모 컴포넌트에 의존하지 않는다.

4. 다른 페이지에 컴포넌트를 그대로 이식해도 성립된다.

5. 컴포넌트 내에 데이터의 획득, 갱신이 완결된다.

 Templates와 Pages 컴포넌트는 페이지 전체에 영향을 끼친다. Templates는 페이지의 대략적인 배치(와이어 프레임), Pages는 컨텐츠의 제약(데이터의 획득, 갱신등의 로직만)을 관활한다.

 Templates는 Presentational, Pages는 Container로 바꿔 말할 수 있다. 또한, PAges는 기본적으로 1화면에 1개가 필요하지만, 레이아웃이 동일하면 Templates는 여러개의 Pages에서 공유하도록 해도 괜찮다(예를 들어, 신규 유저 정보 입력과 유저 등록 정보 갱신과 같이 로직이 미묘하게 다르지만 봤을 때 거의 동일한 경우).

 

 


 예를 들어, 로그인 페이지를 Atomic Design 베이스로 컴포넌트화해보자. 

  페이지는 아래의 그림과 같이 제목, 메일 주소 입력, 패스워드 입력, 송신 버튼으로 구성되어 있다.

 먼저, Pages 디렉토리에 로그인 페이지의 컴포넌트를 만든다. router에서 불러진 컴포넌트는 이 Pages컴포넌트가 된다.

 이번의 예에서는 페이지 표시전에 데이터 획득 등을 하지 않으므로 로직은 작성하지 않고, 전체의 레이아웃을 정의하는 templates/LoginPage.vue를 읽어들일뿐이다.

 /* pages/LoginPage.vue */

<template>
  <LoginPage />
</template>
<script>
import LoginPage from '@/templates/LoginPage.vue'

export default {
  components: {
    LoginPage
  },
}
</script>

 Templates 컴포너틑에서는 표시된 UI를 기재한다.

/*templates/LoginPage.vue*/

<template>
  <section>
    <h1>ログイン</h1>
    <p>アカウントをお持ちの方は<br>下記よりログインしてください。</p>
    <form @submit.prevent id="login">
      <label for="mail">メールアドレス</label>
      <input id="mail" v-model="mailValue" type="text" placeholder="sample@mail.jp">

      <label for="password">パスワード</label>
      <input id="password" v-model="passwordValue" type="password" autocomplete="off">

      <button type="button" @click="clickLogin">ログイン</button>
    </form>
    <p>アカウントをお持ちでない方は<br>会員登録が必要です。</p>
    <p><a href="#">新規会員登録</a></p>
  </section>
</template>

<script>
import { login } from '@/api/user.js';
export default {
  methods: {
    clickLogin(e) {
      e.preventDefault();
      login({ mail: this.mailValue, password: this.passwordValue });
    },
  },
}
</script>

 "로그인" 버튼을 누르면, 입력한 메일 주소와 패스워드를 API에 던지는 것을 상정하고 있다.

 Templates에서는 데이터의 갱신을 하지 않는 것이 규칙이므로 API 통신은 Pages나 Organisms에 실행할 필요가 있다.

 이 페이지의 로그인 폼은 독립하고 있으므로(부모 컴포넌트와 연계가 필요하지 않음 혹은 다른 페이지에 그대로 이식할 수 있는 레벨의 완결된 내용) 로그인 부분은 Organisms으로 나눌 수 있다.

 Organisms으로 하면 데이터의 갱신이 가능하므로, store에 액세스하거나, API로 데이터를 송신하는 것이 가능하다.

 위의 templates/LoginPage.vue에서 Organisms에 폼 부분을 잘라, "로그인" 버튼이 눌려지면 userAPI에 통신하도록 한 것이다.

/*organisms/LoginForm.vue*/

<template>
  <form @submit.prevent id="login">
    <label for="mail">メールアドレス</label>
    <input id="mail" v-model="mailValue" type="text" placeholder="sample@mail.jp">

    <label for="password">パスワード</label>
    <input id="password" v-model="passwordValue" type="password" autocomplete="off">

    <button type="button" @click="clickLogin">ログイン</button>
  </form>
</template>

<script>
import { login } from '@/api/user.js';
export default {
  methods: {
    clickLogin(e) {
      e.preventDefault();
      login( { mail: this.mailValue, password: this.passwordValue });
    },
  },
}
</script>

 잘라낸 organisms/LoginForm.vue를 Templates에서 읽어들이도록 변경한다. Templates는 이것으로 완성이다.

/*templates/LoginPage.vue*/

<template>
  <section>
    <h1>ログイン</h1>
    <p>アカウントをお持ちの方は<br>下記よりログインしてください。</p>
    <LoginForm />
    <p>アカウントをお持ちでない方は<br>会員登録が必要です。</p>
    <p><a href="#">新規会員登録</a></p>
  </section>
</template>

<script>
import LoginForm from '@/organisms/LoginForm.vue'

export default {
  components: {
    LoginForm
  },
}
</script>

 또한, 로그인 폼의 안의 메일 주소, 패스워드 입력은 다른 페이지에도 여러 번 쓸 수 있으므로, 각각을 컴포넌트화하는 것이 좋다.

 외부에서 데이터 갱신을 하지 않고, 2개 이상의 HTML 요소이므로 Molecules 컴포넌트로 한다. Moleculs에서 이참에 폼의 발리데이션(검증)처리도 구현한다.

/*molecules/inputMail.vue*/

<template>
  <div>
    <label for="mail">メールアドレス</label>
    <input
      id="mail" :value="mailValue" type="text" placeholder="sample@mail.jp"
      @change="changeMail"
    >
    <p v-if="error" id="errorMail">{{ error }}</p>
  </div>
</template>

<script>
import { checkMail } from '@/module/validation.js';

export default {
  data() {
    return {
      mailValue: '',
      error: '',
    }
  },
  methods: {
    changeMail(e) {
      const val = e.target.value;
      if (checkMail(val)) {
        this.mailValue = val;
        this.error = '';
      } else {
        this.mailValue = '';
        this.error = 'メールアドレスを正しい形式にしてください';
      }
      this.$emit('handle-change-mail', this.mailValue);
    },
  },
}
</script>

 입력 부분을 Molecules으로 하면 organisms/LoginForm.vue는 아래와 같이 변경된다. 입력 검증의 결과에 따라 버튼의 disable을 제어할 수 있도록하였다.

/*organisms/LoginForm.vue*/

<template>
  <form @submit.prevent id="login">
    <InputMail @handle-change-mail="changeMail">
    <InputPassword @handle-change-password="changePassword">
    <button type="button" :disabled="buttonDisabled" @click="clickLogin">ログイン</button>
  </form>
</template>

<script>
import { login } from '@/api/user.js';
import InputMail from '@/molecules/InputMail.vue';
import InputPassword from '@/molecules/InputPassword.vue';

export default {
  data() {
    return {
      mailValue: '',
      passwordValue: '',
    }
  },
  computed: {
    buttonDisabled() {
      return !!(this.mailValue && this.passwordValue);
    }
  },
  methods: {
    clickLogin(e) {
      e.preventDefault();
      login( { mail: this.mailValue, password: this.passwordValue });
    },
    changeMail(val) {
      this.mailValue = val;
    },
    changePassword(val) {
      this.passwordValue = val;
    },
  },
}
</script>

 input 요소와 button 요소는 다른 페이지에도 거의 동일한 동작을 하므로, Atoms 컴포넌트로 해도 좋을 것이라고 생각된다.

 방금의 그림에 컴포넌트 분할 단위를 추가하면 다음과 같이 표시할 수 있다.

 실제로 각 요소를 컴포넌트화할때는 위에서 아래로, Pages > Templates > Organisms > Moleculs > Atoms의 순서로 생각하면 컴포넌트를 세부적으로 분할하지 않고, 작업하기 쉽게 된다.

 또한, 컴포넌트가 거대해져, 여러 개의 처리가 혼잡하게 된다면 점점 Organisms나 Molecules에 컴포넌트 분할해나간다면 좋을 것이라고 생각된다.

 에러 핸들링은 Organisms에 에러 다이어로그의 컴포넌트를 만들어, 그것을 Templates에 반드시 읽어들이도록 하였다. 에러 다이어로그가 발생한 타이밍은 데이터의 갱신을 했을 때에 한정되므로, 에러 발생시는 Pages 혹은 Prganisms에서 에러용 vuex를 갱신하고, 표시 상태를 제어하도록 하였다(로딩도 비슷한 느낌으로 구현했다).

 

 

디렉토리의 구조


 마지막으로 컴포넌트 설계의 디렉토리 구조에 대해 간략히 살펴보자.

디렉토리 내용
js  
├ api axios 에서의 ajax 통신, web 스토리지로의 액세스
├ atoms button, input등
├ module 범용함수. 검증이나 userAgent 판정등
├ molecules 입력폼(제목+부품), 슬라이더, 카드형 UI등
├ organisms 로그인,  에러, 로딩, notification등
├ pages 페이지 콘텐츠
├ store 로그인 정보, 회원 정보
└ templates 페이지의 틀

참고자료

https://qiita.com/d2cid-kimura/items/4aee84da42131f40b808

728x90