컴포넌트 분리(프론트엔드 아키텍쳐) - 소음 프로젝트 수정(1)

이영섭·2024년 12월 25일

오늘은 소음 측정 화면을 수정해보려 시도하였다.

왼쪽과 같은 이미지의 소음 측정 화면이 오늘 코드로 수정하려 했던 화면인데, 대회에서 진행했던 코드를 살펴보면 2가지 컴포넌트로 나누었다.
1. Chart => 이미지상 회색 박스
2. 측정 화면 페이지
그러나, 실상 페이지를 나타내는 코드를 여유를 가지고 살펴보면 컴포넌트의 목적에 맞게 제대로 분리했는가 의문이 드는 코드였다.

실제 코드는

import React, { useEffect, useState } from 'react';
import DecibelChart from '../../component/decibelChart/DecibelChart';
import { useDispatch } from 'react-redux';
import { setPosition } from '../../store/data/dataSlice';

const Noise = () => {
  const [address, setAddress] = useState<string>('');
  const dispatch = useDispatch();

  const fetchAddressFromCoords = () => {
    navigator.geolocation.getCurrentPosition(
      async (position) => {
        const { latitude, longitude } = position.coords;
        console.log('위도:', latitude, '경도:', longitude);
        dispatch(setPosition({x: latitude, y: longitude}));

        const REST_API_KEY = '83ce629a6d7b809e79dc0b269d5a78c9'; // 카카오 REST API 키
        const url = `https://dapi.kakao.com/v2/local/geo/coord2regioncode.json?x=${longitude}&y=${latitude}`;

        try {
          const response = await fetch(url, {
            method: 'GET',
            headers: {
              Authorization: `KakaoAK ${REST_API_KEY}`, // 인증 헤더
            },
          });

          if (!response.ok) {
            throw new Error(`HTTP error! Status: ${response.status}`);
          }

          const data = await response.json();
          if (data && data.documents && data.documents.length > 0) {
            let address = data.documents[0].address_name; // 첫 번째 주소를 가져옴
            console.log('주소:', address);
            address = address.slice(0, 7);
            setAddress(address); // 화면에 주소 표시
          } else {
            console.error('주소 데이터를 찾을 수 없습니다.');
          }
        } catch (error) {
          console.error('API 호출 중 오류 발생:', error);
        }
      },
      (error) => {
        console.error('위치 정보를 가져오는 중 오류 발생:', error);
      }
    );
  };

  useEffect(() => {
    fetchAddressFromCoords(); // 컴포넌트가 마운트될 때 호출
  }, []);



  return (
    <div>
      <DecibelChart address={address}/>
    </div>
  );
};

export default Noise;

이러한 코드 구성으로 이루어져있었다. 실질적으로 위치정보를 가져오는 로직을 제외하고, UI를 구성하는 코드는 별로 없다는 인상을 받았다.

첫 프로젝트 진행 당시에는 정반대로 한 페이지에 대해 컴포넌트를 최대한 분리시켜 진행시키려다 props 전달에 애를 먹었던 적이 있었다. 아래의 이미지는 첫 프로젝트의 나의 담당 페이지로 빨간 표시가 컴포넌트로 나눈 것이었다.

탭을 나타내는 컴포넌트, 게시글, 게시글의 반응, 게시글 페이지, 댓글 버튼을 눌렀을 때 입력창, 각 댓글, 댓글리스트를 나타내는 페이지로 대략 7개의 컴포넌트로 나누었었다.
과연 내가 목적에 맞게 위 2개의 컴포넌트를 분리했는지 컴포넌트 분리의 목적에 대해 다시 상기할 필요가 있었다.

컴포넌트의 분리 목적
1."관심사의 분리" 2."코드 재사용성 증가" 3."유지보수의 용이성 향상"을 위함이다.
프로젝트를 진행하면서 코드 재사용성 증가, 유지보수의 용이성 향상은 충분히 와닿았다.

위 2개의 프로젝트에서 단순히 여러 페이지에서 재사용하기 위해 컴포넌트를 분리시킨 적도 있었다. 하지만 게시글 리스트, 소음 측정 데이터 리스트와 같이 각 리스트를 나타낼 페이지와 map 메소드를 통해 반복적으로 뿌릴 리스트들도 컴포넌트로 분리시킨 것은 다음과 같은 이유였다. 게시글 페이지는 전체적인 레이아웃을, 게시글 리스트는 각 게시글 데이터를 받아와 랜더링하는 역할을 맡아 책임을 명확히 함으로써 "유지 보수의 용이성 향상"을 위한 것이라 생각된다.

다만 고민했던 점은 프로젝트 초기에 와이어프레임만을 보고, 명확한 목적과 기능을 가진 하나의 컴포넌트로 분리하기 위해 어떤 확실한 기준을 적용해야 할지에 대한 것이었다.

컴포넌트의 분리 목적 자체는 이해할 수 있고, 프로젝트 완료 시점에서 목적자체가 와닿지만, 프로젝트 진행 시기에는 개발 상황에 따라, 변수에 따라 막막한 경우가 많기 때문이다.

소음 프로젝트 수정 전에 기준을 가지고 분리시킬 컴포넌트를 구분짓는 것이 좋을 것이라 생각해서 분리 기준에 대한 조사를 하였다.

컴포넌트 분리 기준

1. 단일 책임 원칙 (Single Responsibility Principle)

단일 책임 원칙은 소프트웨어 디자인 원칙 중 하나로, 모든 클래스나 컴포넌트는 하나의 기능만을 담당해야 한다고 강조합니다. 이 원칙을 따르면, 각 컴포넌트는 하나의 작업만 수행하므로, 수정이 필요할 때 해당 컴포넌트만을 고치면 되기 때문에 유지보수가 쉬워집니다.

1) 컴포넌트가 여러 책임을 갖는 경우

여러 책임을 갖는 컴포넌트는 컴포넌트 분리 없이 만든 거대한 페이지 컴포넌트와 비슷합니다.

function Page(props) {
  // 선택한 탭을 변경하면 보여주는 내용을 변경합니다.
  // 페이징을 다룹니다.
  // 단어를 검색을 합니다.
  // 검색 조건 토글을 다룹니다.
  // 등등
}

이렇게 되면 기능 간에 결합이 강하게 발생해서 수정이 쉽지않습니다. 이건 마치 삼체문제와 비슷해서 서로 상호작용하는 기능이 많아지는 것보다 문제의 복잡성이 더 빨리 어려워진다는 것을 의미합니다.

그렇기 때문에 컴포넌트를 책임에 맞게 나눠서 문제를 단순화 해야 합니다. 따라서 UI와 비지니스 로직을 적절하게 분리하는 건 소프트웨어를 오랫동안 유지보수 하는 데 있어서 아주 중요합니다.

2) 컴포넌트에 비지니스 로직이 있는 경우

일반적으로 유저 인터페이스(UI)와 비지니스 로직은 변경의 속도, 즉 빈도가 다릅니다. 하지만 컴포넌트에 비지니스 로직이 포함되어있다면 빈번한 UI 변경에 따라 자주 영향을 받을 수 있습니다.


2. 재사용 가능성 (Reusability)

재사용 가능성은 코드의 일부분을 다른 프로젝트나 다른 부분에서도 사용할 수 있도록 하는 것을 목표로 합니다.
재사용 가능하다는 것은 그만큼 일반적이라는 것을 의미합니다.
예를 들어 '탈것'이라는 개념이 '자동차', '자전거', '말'보다는 보편적입니다. 우리는 컴포넌트의 재사용성을 고려할 때 일반적이라는 두 가지 측면 중 ‘속성이 보편적’인지 고민합니다. 즉, ‘다른 컴포넌트가 가져가서 사용할 수 있도록 보편적인 속성을 갖고 있는가?’를 고려합니다.

중복을 고려한 재사용성

우리가 컴포넌트의 재사용성에 대해 말할 땐 보통 중복을 떠올립니다. 두 곳 이상에서 중복된 무언가 존재하고 ‘추출’하는 과정을 거칩니다. 그리고 추출한 것을 재사용합니다.
예시) 게시글 리스트

  • 게시글 페이지
function PostList({ posts }) {
    return (
        <ul>
            {posts.map(post => (
            	<li key={post.id}>
                	<{Post post={post} />
                </li>
            ))}
        </ul>
    );
}
  • 게시글 리스트
function Post({ post }) {
    return (
        <>
           {post.author} - {post.content}
        </>
    );
}

3. 가독성 (Readability)

코드의 가독성을 높이는 것은 프로그램의 이해를 돕고, 다른 개발자가 코드를 더 쉽게 읽고 수정할 수 있게 합니다. 작고 독립적인 컴포넌트로 코드를 나누면, 각 부분의 기능이 명확해지며, 에러를 찾기 쉬워지고, 전체 구조를 이해하기 쉬워집니다.

4. 상태와 라이프사이클 (State and Lifecycle)

컴포넌트의 상태 관리와 라이프사이클은 특히 리액트 같은 UI 라이브러리에서 중요한 개념입니다. 컴포넌트는 자신의 상태를 내부적으로 관리할 수 있고, 상태 변화에 따라 적절한 라이프사이클 메서드가 호출됩니다. 이러한 메서드는 컴포넌트가 화면에 렌더링되기 전 후, 업데이트 되기 전 후, 제거되기 전에 실행되는 코드를 정의합니다.

5. UI 요소 (UI Components)

다양한 UI 요소를 별도의 컴포넌트로 분리하면, 각 요소가 독립적으로 개발되고 테스트될 수 있습니다. 이는 UI의 일관성을 유지하고, 복잡성을 관리하는 데 도움이 됩니다. 예를 들어, 버튼, 입력 필드, 레이아웃 구조 등을 각각 독립된 컴포넌트로 개발하여 필요한 곳에서 쉽게 재사용할 수 있습니다.

이러한 기준들을 통해 구현 과정에서의 복잡성을 줄이고, 더욱 효율적이고 유지보수가 쉬운 시스템을 구축할 수 있습니다.

참조 및 인용1 - 프론트엔드-아키텍처-컴포넌트를-분리하는-기준과-방법
참조 및 인용2 - [React]컴포넌트 분리(기준, 방법, 중요성)

그렇다면 위의 기준을 가지고 나의 소음 측정 페이지를 어떻게 컴포넌트로 분리시킬 것인지에 대해 고민하였다.

내가 생각한 분리된 컴포넌트는 위 이미지의 빨간 표시로 되어 있는 것과 같다. 단일 책임 원칙에 따라 시간을 나타내는 컴포넌트와 위치를 나타내는 컴포넌트, 차트를 나타내는 컴포넌트, 측정 시간을 나타내는 컴포넌트로 나타내는 것이다.
대략적인 모식도는 다음과 같다

여기서 비즈니스 로직을 분리하고, UI요소를 분리해내기 위해 유저의 사용 과정을 생각할 필요가 있다.
해당 과정은 아래 이미지와 같다.

위의 이미지처럼 information icon을 클릭하면 소음 영향에 대한 기준이 나타나고, 현재 시각, 현재 위치가 나타나게 된다. 변화가 많기 때문에 각 요소별로 어떻게 변화하는지 살펴보려 한다.

버튼

측정 화면에서 측정 시작 버튼을 누르면 버튼이 측정 취소 버튼으로 변경되고, 15초가 지날시 다시 버튼은 측정 저장 버튼과 측정 취소 버튼이 나타난다. 또한 마지막 이미지 상 데시벨 측정 정보를 저장하는 과정에서 사용되는 버튼 역시 글자의 스타일만 다를뿐 버튼의 크기는 동일해서 Routing 경로에 따라 글자를 달리 해주어야 할것으로 예상된다.

시간

평소에는 현재 시간을 나타내다가 측정 시작 버튼 클릭시 측정시작 시간으로 멈추게 되며, 다시 측정 취소 버튼 클릭시 현재 시간을 나타내게 된다.

위치

현재 좌표를 받아내 현재 위치를 표현하면 된다.

평균, 현재, 최대 dB

실시간으로 dB을 측정하여 현재 dB에 반영하며 평균과 최대dB을 계산한다. 현재 dB에 비해 상대적으로 평균과 최대 dB의 변화는 적을 것이라 생각된다.

마커

평소에는 회색의 마커로 표시되다가 decibel을 측정하게 되면 평균 dB에 따라 마커가 변화하게 된다.

차트

현재 dB을 측정하여 라인 차트가 그려지게 된다.

측정 시간

평소에는 00:15를 보여주다 측정이 시작되면 시간이 초단위로 감소하게 되고, 15초가 지날시 +00:00부터 초단위로 증가하게 된다.

위와 같은 흐름에 따라 전체적인 컴포넌트 구성을 다음과 같이 생각하게 되었다.

전체적인 컴포넌트 구성

직접 코드를 작성 혹은 리팩토링에 따라 컴포넌트 구성이 달라질 수 있겠지만 현재 피그마 디자인과 내가 대략적으로 구성한 코드를 보고 생각한 컴포넌트 구성이다. 사실 평균 데시벨과 최대 데시벨 컴포넌트는 나눌 것인가 아니면 현재 데시벨, 평균 데시벨, 최대 데시벨을 그룹화하여 컴포넌트로 구성할 것인지 고민이 많았다. 평균 데시벨과 최대 데시벨 컴포넌트로 나눌 시 불필요하게 컴포넌트로 나누는 것이라 생각했기 때문이다. 하지만 현재 데시벨과 차트는 실시간으로 변화가 많기 때문에 평균과 최대 데시벨과 그룹화하여 컴포넌트로 만들 경우 불필요하게 더욱더 랜더링될 것이라 판단했고 컴포넌트를 추가로 생성시 확장성이 좋아질 것이라는 기대감때문이었다.

다음에는 이 생각을 기반으로 코드 수정에 들어갈 것이다.

profile
신입 개발자 지망생

0개의 댓글