Juun

  • About
  • Portfolio
  • Blog
  1. Blog
  2. Separation Of Concerns

Separation of Concerns in Frontend Development

Exploring the principle of Separation of Concerns in frontend development and its impact on code maintainability and scalability.

English Version

The Concern

In computer science, separation of concerns (SoC) is the design principle of organizing a codebase into distinct sections - each addressing a single concern.

- Wikipedia

SoC (Separation of Concerns) 는 소프트웨어 설계 원칙 중 하나로, 코드베이스를 서로 다른 관심사(Concerns)로 나누어 각 부분이 독립적으로 관리되고 개발될 수 있도록 하는 것을 의미한다. SRP(Single Responsibility Principle)와도 유사한 개념이기 때문에 혼동할 수 있지만, SoC 의 분리 기준인 Concern 은 흔히 기능 단위로 통용되는 SRP 의 책임 보다 훨씬 넓은 개념을 포괄적으로 다룬다. 이 글에서는 Next.js 프론트엔드 프로젝트에서 SoC 를 어떻게 적용했는지를 중심으로 이야기해보고자 한다.

What Is Concern?

그래서 concern 이란 무엇일까. 좁은 의미에서는 관련된 Building Block (module, class, component, function 등) 이 담당하는 기능 단위로 볼 수 있다. 하지만 하나의 Building Block 은 여러 concern 을 가질 수 있다. 예컨데, 비즈니스 로직을 담당함과 동시에 modularity 달성이라는 목적도 내포할 수 있는 것이다. 여기서 modularity 달성은 Building Block 의 이식성(portability), 재사용성(reusability) 등 기능 외적인 측면에 대한 concern 이다. 이는 organizational concern(조직 관심사), 또는 quality concern(품질 관심사) 로 분류할 수 있다. 이처럼 concern 은 기능적인 측면과 비기능적인 측면 모두를 포괄하는 넓은 개념이다.

Why it Matters

Concern 은 building block 을 바라보는 시각, view point 에 따라 범위와 성격이 달라지며, 이에 따라 분리의 기준도 변화한다. building block 에 어떤 concern 을 부여할 것인지, 그리고 그 concern 에 따라 building block 을 어떻게 나눌 것인지가 결정되고, 관련된 concern 들을 조율해나가며 building block 의 형태가 구체화된다. 이는 곧 software 의 전체적인 architecture 에 영향을 미친다. SoC 는 결국, concern 을 정의하고 구체화하는 과정에서 자연스레 발생하는 작업인 것이다.

Frontend-Specific Concerns

언급한 바와 같이, concern 은 관점에 따라 다양한 형태로 나타날 수 있으며, 여기에는 framework 또는 domain-specific concern 도 포함된다. 여기서는 frontend, 그 중에서도 Next.js 환경에서 고려해야 할 concern 에 대해 살펴보고자 한다. 아래에서 다루는 concern 의 명칭은 본인이 임의로 부여한 것이며, concern 은 정의하기 나름이라는 것을 명심하자.

Layout Concerns

전통적인 웹 애플리케이션에서는 페이지의 시각적, 개념적 구조를 정의하는 layout 이 html 및 css 가 담당하여, Accordion 의 토글 상태, Modal 의 열림/닫힘 상태 등과 같은 UI 상태는 css class, 또는 data attribute 를 통해 관리되었다. 그러나 React 와 같은 component-based framework 는 UI 의 상태를 함께 관리하게 되면서, 분리 되어있던 layout concern 이 결합된 양상을 띠게 되었다. Headless UI 와 같이 로직과 스타일을 분리하려는 시도가 있지만, 컴포넌트의 구조적 계층이나 상태의 시각적 표현 방식 등은 여전히 개발자가 결정해야 하는 영역으로 남으며, 이는 frontend 구성에 영향을 주는 influence factor 로 작용하여 layout concern 에 대한 고려가 추가로 필요해졌다.

State Concerns

상태에 대한 변화를 감지하는 Observer Pattern 을 기반으로 하는 React framework 에서는 상태 관리(state management) 가 중요한 concern 중 하나이다. 하나의 상태가 여러 컴포넌트에 영향을 미칠 수도 있고, 애플리케이션 전체에 영향을 줄 수도 있지만, 반대로 어떤 컴포넌트는 상태를 전달 받아 표시만 하고, 변경하는 역할을 하지 않을 수도 있다. State 의 관리 방법, 범위 설정 등에 따라 컴포넌트의 구성이 달라지고, 어느 컴포넌트가 layout concern 을 담당할지에 대한 결정에도 영향을 미친다. State concern 은 비즈니스 로직과도 관련되고, 다른 범위의 concern (quality, organizational) 도 내포할 수 있기 때문에, state concerns 의 정의가 frontend concerns 의 핵심 중 하나라고 볼 수 있다.

Other Concerns

이외에도 다양한 concern 이 존재한다. 그 중 하나는 위에서 살펴본 React framework 처럼, 사용하는 framework 나 기술 스택이 그 자체로 concern 이 되는 경우이다. Framework 또는 API, Library 등을 사용하기 위한 인터페이스 자체가 고려대상이 되는 경우로, 다양한 npm packages 로 구성되는 현재 frontend 생태계에서는 고려하지 않을 수 없는 부분들이다. 이는 contract interface 의 특성에 따라 또다른 concern (e.g. performance) 을 발생시키기도 한다.

또 다른 concern 으로는, web design 및 cooperation concerns 가 있다. 웹 디자인의 적용 및 디자이너와의 협업 프로세스도 architecture 에 반영될 수 있는 요소로 간주하고 고려되어야 한다.

Case Study

이 글을 호스팅하는 프로젝트 페이지 중, cesium-utils 패키지의 feature demo 페이지에 대한 리팩토링 과정을 통해 frontend 에서 발생할 수 있는 concern 을 소개한다.

다시 한 번 강조하지만, 이는 본인이 적용한 concern 일 뿐이며, 상황에 따라, 환경에 따라 concern 의 정의는 달라질 수 있다.

Cesium Utils Refactoring

리팩토링 이전의 구조는 보편적인 Next.js (React) 프로젝트의 구조를 따르고 있었다. 컴포넌트의 기능적인 역할에 따라 폴더 트리를 생성하고, router 를 담당하는 app 폴더는 page 와 layout 등만 포함하는 구조였다.

text
1apps/web/
2├── app/
3│   ├── cesium-utils/
4│   │   ├── page.tsx              # Router - main page
5│   │   └── layout.tsx            # Router - layout only
6│   └── ...
7├── components/
8│   ├── cesium/
9│   │   ├── viewer.tsx            # Cesium viewer component
10│   │   ├── api-combobox.tsx      # API select ui
11│   │   └── ...
12│   └── ...
13├── contexts/
14│   └── ...
15├── lib/
16│   ├── stores                    # Zustand stores, global state management
17└── utils/
18│   └── ...                       # Utilities
19└── ...

Cesium 의 viewer 는 Zustand store 에 등록하여 접근하며, app 하위의 폴더에서 필요한 컴포넌트들을 사용하는 일반적인 구조이다. 이 구조에 대해서는, 컴포넌트들의 grouping 을 위해 components 폴더 아래에 route 폴더와 중복되는 이름이 생길 수 밖에 없는 한계를 느끼고 있었다.

text
1apps/web/
2└── app/
3    └── cesium-utils/
4        ├── _components/
5        │   └── viewer/
6        │       └── index.tsx     # Lazy imported viewer
7        │       └── viewer.tsx    # Isolated Cesium viewer
8        │   └── ...
9        ├── _contexts/
10        │   └── provider.tsx      # Route-scoped context
11        ├── _layouts/
12        │   └── sidebar.tsx       # Layout component
13        ├── _utils/
14        │   └── api.ts            # Business logic - centralized API config
15        ├── [api]
16        │   └── page.tsx          # Dynamic Router - pages for each APIs
17        ├── page.tsx              # Router - main landing page
18        └── layout.tsx            # Router - provides context to children

리팩토링 후 변경된 구조는 위와 같다. 가장 큰 변화는, route 에서 사용되는 컴포넌트들을 해당 폴더로 모아 응집성을 높였다는 점(High Cohesion)이다. 리팩토링에 영향을 준 concerns 는 다음과 같다.

  1. Framework Concerns

    Next.js 의 private folder pattern, 폴더 명이 언더스코어(_)로 시작하면 app router 폴더 내에 있어도 URL Path 로 노출되지 않는 특수 패턴을 활용했다. 이를 통해 route-specific 컴포넌트들을 모아 폴더 이름 중복을 해결했다. 기존 components 폴더는 여러 route 에서 사용되는 application-specific 컴포넌트들의 집합으로 그 역할을 변경했다.

  2. Organizational Concerns

    업무 중, 서비스의 portability 를 확보하라는 조직에서의 요구사항을 충족시키기 위한 prototype 을 적용했다. 같은 framework, 동일한 개발 환경이라면 route 에 구현된 서비스를 그대로 사용할 수 있게끔 하기 위해 state concerns 를 재정의하고, 필요한 모든 기능을 route 폴더가 담을 수 있도록 했다. 이를 위해서 DRY(Don't Repeat Yourself) 원칙을 다소 위배하더라도 코드베이스를 복사하여 분산시킬 필요가 있었다. 다소의 중복을 허용하면서, 관련 컴포넌트들의 참조를 route 내에서 일어나게 하여, 해당 폴더 내의 변화가 다른 route 의 서비스에 영향을 미치지 않도록 분리했다.

  3. State Concerns

    viewer 는 그 특성 상, 관련 route 내에서만 상태 정보에 대한 접근을 요구했다. 이에 따라 zustand 를 사용하던 기존 방식에서, route-scoped context 로 전환했다. 또한 cesium-utils 패키지에 대한 demonstration 을 보여주는 route 의 기능 특성을 반영하여, API 추가 및 관리가 용이하도록 API State (Configuration) 의 centralize 를 진행했다. 현재 관련 컴포넌트들은 기존의 각각 API 목록을 갖고 있던 방식에서 _utils 에서 선언된 API & Feature Configuration 에 따라 목록 표시 및 기능을 수행하는 형태로 변경됐다. API 목록에 대한 상태 관리를 위임한 것이다.

  4. Layout Concerns

    이 부분은 최근에 들어서야 분리할 필요성을 느꼈다. sidebar 와 같은 페이지의 구조를 책임지는 컴포넌트를 식별하고, SSR (Server Side Rendering) 구성이 가능한, state concerns 에서 벗어날 수 있는 컴포넌트들로 구성했다.

  5. Performance Concerns

    Cesium 은 Web 3D geospatial 엔진으로, 그 상당한 번들 크기 때문에 First Load JS 에 큰 영향을 준다. 이 route 에 접근하지 않는 사용자는 해당 번들이 전혀 필요가 없는데도 불구하고, static import 로 선언된 번들은 모든 사용자가 받아야 하기 때문에 비효율적이라고 느껴졌다. 실제로 기록된 metric 에서도 First Contentful Paint 가 나타나기까지의 시간이 오래 걸리는 문제가 발생했다. 이는 Bundle Optimization 을 통해 해당 route 에서의 모든 Cesium 관련 컴포넌트들을 Lazy Import 처리함과 동시에 private folder pattern 으로 모아서 High Cohesion 을 달성할 수 있었다.

이 과정에서, separation 은 크게 중요하지 않았다. 각 building block 에 관련된 concern 들을 선별하고 구체화하는 과정에서 SoC 는 자연스럽게 발생했을 뿐, 분리 그 자체가 목적인 과정은 아니었다.

State Flow Diagram

더 자세한 구현 과정을 react-query 를 사용하는 API 응답 결과 표시 컴포넌트에서의 state 흐름 및 SoC 적용을 공유하기 위해 그렸던 diagram 으로 살펴보자.

Wrapper Component 는 API 에 대한 요청 및 응답에만 관여한다. 이는 하위 (Rendering) 컴포넌트들에 대한 state 값을 조절해준다는 의미에서 State Coordinator 라고 표시했다. 해당 컴포넌트가 API Request 에 필요한 Parameters 의 초기 값을 props 형태로 전달 받고, 가변 값으로 설정한 parameter 를 state 로 감싸 하위 컴포넌트로 전달한다.

하위 (rendering) 컴포넌트 들은 state 값과 set state action 을 모두 전달 받아 사용하고, 이 컴포넌트들 사이에는 state 의 영향이 전혀 없도록 구성한다. 이 구성은 SoC 와 동시에, SRP 도 함께 달성했다고도 볼 수 있다.

Real-World Implementation

하지만 위의 diagram 은 실제 구현 이전에 그려진 스케치에 불과하다. 실제 구현에는 더 정교한 SoC 가 적용됐다.

data-table.tsx
1export default DataTable({
2// Immutable initial parameters
3...props
4}) {
5  // Mutable UI state
6  const [pageNo, setPageNo] = React.useState(props.pageNo);
7  const [numOfRows, setNumOfRows] = React.useState(props.numOfRows);
8
9  // API integration with react-query
10  const { data } = useQuery({
11    queryFn: () => api.fetch({
12      ...props, // spread immutable
13      pageNo, // override with mutable
14    })
15  });
16
17  // Derived state from data
18  const columns = React.useMemo(() => createColumns(data), [data]);
19
20  // Orchestrated state updates
21  const handleRowsChange = (rows) => {
22    setNumOfRows(rows);
23    setPageNo(1); // Side effect coordination
24  };
25
26  return (
27    <React.Fragment>
28      {/* Rendering Component */}
29      <ViewTable data={data?.result.resultList} columns={columns} />
30      {/* Rendering Component */}
31      <Pagination
32        pageInfo={data?.result.pageInfo}
33        onPageNoChange={setPageNo}
34        onNumOfRowsChange={handleRowsChange}
35      />
36    </React.Fragment>
37  )
38}

실제 구현에서는 다음과 같은 SoC 가 추가로 발생한 것을 확인할 수 있다.

  • Data Fetching: useQuery 가 담당
  • UI(Mutable) State: local state 로 관리
  • Data Transformation: useMemo를 통해 react-table 에 호환 가능한 형태로 변환 및 memoization
  • State Coordination: Immutable / Mutable Parameters 의 merge 및 override 처리를 wrapper 에서 담당

Conclusion

여기까지, SoC 의 기준이 되는 concern 과 적용 사례를 소개해봤다. 계속 언급했던 내용처럼, 소개한 것은 본인 사례에 국한된 concern 이며, concern 은 정의하기 나름이다. 이를 구체화하는 과정에서 SoC 가 일어나기도 하고, 소프트웨어 전반에 걸쳐 영향을 주는 Cross-Cutting Concern 이 발견되기도 한다. 어떻게 정의하고 무엇을 적용하느냐에 따라 완전히 다른 결과물이 나올 수도 있고, 전혀 다른 concern 으로부터 출발해 비슷한 결실을 맺는 경우-예를 들어, Cloud Native와 Android OS는 서로 다른 목적(확장성 vs Device 최적화)에서 출발했지만 유사한 분산 아키텍처로 수렴했다-도 있다. 역설적이게도, concerns 를 정의하는 자체는 중요하지 않을 수 있다. 이는 설계 과정에서 식별할 수 있는 concern 과 구현 과정에서 발생하는 concern 이 다를 수 있기 때문이다. 하지만 concerns 자체가 architecture 에 영향을 주는 것은 분명하기에, 이미 식별된 concerns 는 구성원들 사이에서 충분한 이해를 바탕으로 공유되어야 한다고 생각한다.

Date

October 1, 2025

Tags

frontendarchitectureseparation-of-concernSoCweb-development

Share article

Separation of Concerns in Frontend Development
State flow diagram - the wrapper component
State flow diagram - react query data flow