본문 바로가기

JavaScript

컴포넌트는 어떻게 등장하게 되었는가

반응형

https://velog.io/@teo/separation-of-concerns-of-frontend

 

프론트엔드 개발자 관점으로 바라보는 관심사의 분리와 좋은 폴더 구조 (feat. FSD)

최근 프론트엔드 개발에서 주목받는 FSD 아키텍쳐 폴더 구조를 주제로 소프트웨어 공학 관점에서의 관심사의 분리라는 원칙을 통해 설명하고자 했습니다. 이 글은 그동안 프론트엔드가 복잡성

velog.io

 

우리는 언어를 3개나 배운다! 😆


웹 개발을 하기 위해서 우리는 3가지 언어나 배워야 합니다.
HTML과 CSS 그리고 Javascript 죠.
어쩌다가 웹 개발은 언어를 3개나 하게 되었을까요?

HTML은 웹을 표현하는데 있어서 상당히 유용한 언어입니다.
한번 HTML없이 javascript만으로 웹페이지를 만든다고 생각해볼까요?

<div>hello, world</div>
const div = document.createElement("div")
const textContent = document.createTextNode("hello, world")
div.appendChild(textContent)
document.body.appendChild(div)

위와 같이 웹 개발자에게는 화면을 만들기 위해 HTML과 CSS가 필수적입니다.

웹은 문서를 만들기 위해서 시작했어요!


그렇다면 HTML, CSS, JS는 나눠진걸까요?

사실 처음부터 이렇게 구조, 표현, 동작을 분리해서 만들려고 한 것은 아니었습니다.

  • 원래는 기능 없이 문서로만 보여주기 위해 구조를 잘 표현할 수 있는 언어(HTML)를 사용했고,
  • 여기에 서식을 추가하며 서식 전용 언어(CSS)가 등장했습니다.
  • 이후 동적으로 콘텐츠를 수정할 수 있는 언어(Javascript)가 결합되면서

현재의 HTML, CSS, JavaScript가 탄생한 것입니다.
각 언어는 그 목적에 따라 자연스럽게 발전해왔습니다.

HTML(1993): 문서의 구조를 보여주는 역할을 위한 언어를 만들어서 사용하자.
CSS(1996): HTML을 꾸미기 역할을 담당하는 언어를 만들어서 사용하자.
JS(1995): 동적으로 문서를 변화하는 역할을 하는 언어를 사용하자.

이렇게 하나의 웹을 만들 때 HTML, CSS, JavaScript로 각자의 관심사에 맞게 분리하여 사용하는 것을 "관심사의 분리"라고 합니다. 이는 개발에서 아주 중요합니다.
각 언어가 구조, 표현, 동작이라는 고유한 역할을 담당하면서,
하나의 언어로 개발하던 때에 비해, 복잡한 개발을 좀 더 효율적으로 관리할 수 있게 되었습니다.

관심사의 분리: 각자 맡은 역할이 유리한 방식대로, 각자의 관심을 분리하여 개발해보자!

관심사 분리와 프론트엔드의 탄생


Ajax(Asynchronous JavaScript and XML)의 등장으로, 웹에서 서버와 클라이언트의 역할을 명확히 구분하게 되었습니다. 서버는 데이터를 처리하는 역할을 맡고, 클라이언트는 이 데이터를 화면에 그리는 역할을 하게 되었죠.

Ajax란?
비동기식 자바스크립트와 xml의 약자로, 웹페이지의 전체 페이지를 새로 고치지 않고 페이지의 일부분만을 서버에서 가지고 와서 "웹페이지 화면을 동적으로 변경하는 방식"
Ajax 예시 1: 구글 맵
Ajax 예시 2: 네이버 자동 검색어

이후 프론트엔드에선, 서버와 데이터 통신, 콘텐츠, 화면 디자인, 사용자의 인터랙션, 데이터 변화에 따른 자동 렌더링 등 다양한 역할이 발전하며 Frontend라는 하나의 관심사가 형성되었습니다. 이처럼 관심사의 분리는 각 요소가 맡은 <역할>을 중심으로 시작하게 됩니다.

컴포넌트 <희망편>: 웹 프레임워크가 가져온 새로운 패러다임의 변화


이제 웹 개발에선 기술 계층으로 나누어 관심사를 분리하게 되었습니다.
이러한 접근은 각 기술의 역할을 명확히 구분하는 데 큰 도움이 되었으나, 개발 과정에서는 하나의 기능을 구현하기 위해 동시에 여러 계층을 오가며 작업해야 하는 불편함이 발생하곤 했습니다.

이런 배경으로, 컴포넌트가 등장했습니다. 컴포넌트특정 기능이나 UI 요소를 구현하기 위한 모든 요소(HTML, CSS, JS)를 하나의 단위로 묶어 다루는 방식입니다. 즉, 함께 사용하고 모아두는 것이 핵심이죠.


컴포넌트는 기존의 수평적 계층 구조를 수직으로 관통하는 새로운 분류 방식을 제시합니다.

케이크를 한 번 생각해볼까요?
기존의 구조는 역할을 중심으로 층층이 쌓아 올린 계층구조였다면,
이제는 기능을 중심으로 조각내어 다시 결합하는 방식으로 발전했습니다.

각 UI 요소와 관련 로직이 하나의 단위로 묶이게 되면서,
재사용성이 증가하고 독립적인 개발과 테스트가 가능해졌습니다.
프론트엔드의 관심사는 프레임워크를 기점으로 역할이 아닌 기능 중심으로 변화하기 시작했습니다.

컴포넌트 <절망편>


그러나 컴포넌트 기반 개발이 모든 문제를 해결해준 것은 아니었는데요,
이번엔 이들을 어떻게 효과적으로 관리하고 찾을 것인지가 새로운 과제로 대두되었죠.

이때의 문제점들을 한번 정리해보자면 다음과 같습니다.

  1. 컴포넌트의 비대화: 하나의 컴포넌트 안에 데이터 관리, 표현 로직, 비즈니스 로직 등 너무 많은 책임이 집중되었습니다. 이는 단일 책임 원칙(SRP)을 위반하는 결과를 낳았죠.
  2. 높은 결합도: props drilling 문제로 인해 컴포넌트 간 결합도가 높아졌습니다. 이는 컴포넌트의 재사용성과 유지보수성을 저해하는 요인이 되었습니다.
  3. 관리의 어려움: 컴포넌트의 수가 늘어나면서 이들을 어떻게 효율적으로 구조화하고 관리할 것인지가 큰 도전 과제가 되었습니다.

결과적으로 우리는, 기능을 중심으로 분리하기 위해서 컴포넌트를 만들어 관심사의 분리를 성공했으나, 결국 하나의 컴포넌트가 너무 많은 역할을 수행하게 된 것입니다. 다시 관심사를 분리할 때가 왔습니다.

컴포넌트의 잘못은 아니야. 그렇지만 다시 계층적 관심사로 돌아가자.


컴포넌트 기반 개발은 분명 혁신적인 접근 방식이었지만,
컴포넌트로 인해 더 크고 복잡한 프로젝트를 할 수 있게 되었지만,
대규모의 프로그램에선 이상적인 컴포넌트 구조를 적용하긴 어려웠습니다.

그래서 우린 컴포넌트 내부와 컴포넌트 간 구조를 더 체계적으로 정립하는 쪽으로 진화하게 됩니다.

즉, 컴포넌트라는 큰 틀은 유지하되, 컴포넌트를 다시 여러 계층으로 나누고,
각 계층별로 명확한 역할과 책임을 부여하는 방식을 택하게 됩니다.

1. UI 컴포넌트의 계층구조 만들기

:Atomic Design Pattern

UI 컴포넌트 계층 구조를 만들기 위해, Atomic Design Pattern을 도입했습니다.
단순히 atoms - molecules - organisms 의 이름과 분리가 중요한 게 아닙니다.
한 곳에 모여있던 컴포넌트들을 분리할 수 있는, 보다 세밀한 기준이 생긴 것입니다.

2. HTML-CSS 의존성 해결하기

: CSS가 HTML을 따라가도록

CSS와 HTML은 다른 역할을 하여 분리했지만, 실제로는 양방향으로 강하게 의존하는 구조입니다.
그렇기에 HTML을 수정하면 CSS가 깨지고, CSS를 변경하면 HTML 구조에 영향을 미치는 일이 다반사였고, 이게 CSS의 가장 큰 문제였습니다.

이를 해결하기 위한 해법으로 의존성의 방향을 단방향으로 만들고자 했습니다. 단순히 역할만 분리하는 것이 아니라 의도적으로 한쪽 방향으로만 의존하도록 하여 관리하기 쉬운 구조를 만드는 것이죠. 최소한 어느 한쪽은 수정을 해도 문제가 없어야 하니까요.

대다수의 경우, CSS가 HTML을 따라갑니다. 디자인은 한 번 만들어지고 나면 자주 바뀌지는 않지만 컨텐츠의 구조는 자주 바뀌니까요. 그 결과 HTML 구조 변경 시 CSS가 자동으로 따라가게 되어, 스타일 깨짐이나 예기치 않은 영향을 최소화할 수 있게 되었습니다.

아래와 같은 기술과 방법론이 등장했습니다:

1. BEM(Block Element Modifier): CSS의 구조와 우선순위를 HTML 구조에 맞춰 정의합니다.
2. CSS Modules: CSS를 JS 모듈처럼 다루어, 컴포넌트와 1:1로 연결합니다.
3. CSS-in-JS: CSS를 완전히 JS 안에 포함시켜, 컴포넌트와 스타일을 하나의 단위로 만듭니다.

3. 비지니스로직 뷰로직의 분리

: State management

컴포넌트의 계층적 구조가 복잡해지면서,
데이터를 효율적으로 관리하고 전달하는 문제도 대두되었습니다.
이를 해결하기 위해 등장한 것이 바로 상태 관리(State Management)였습니다.

상태 관리의 핵심 목적은 다음과 같았습니다:

  1. 컴포넌트의 역할 축소: 컴포넌트는 오직 렌더와 이벤트를 받는 역할만 수행하도록 했습니다.
  2. 데이터 로직의 분리: 복잡한 데이터 관련 로직은 별도의 계층으로 위임하였습니다.

이러한 접근은 entity(데이터 모델)와 view(화면) 사이의 의존성을 정리하고 관심사를 명확히 분리하는 데 큰 도움이 되었습니다.

여기서 중요한 점은 데이터와 화면의 특성 차이였습니다. 데이터 구조는 비교적 안정적이었지만, 화면 구성은 자주 변경될 수 있었습니다. 따라서 우리는 '데이터 → 화면'의 단방향 의존성을 구축해냈습니다. 이렇게 함으로써 데이터의 일관성은 유지하면서도, 화면의 유연한 변경이 가능한 구조를 얻을 수 있었습니다.

4. 서버 상태 관리

: 특수한 계층은 역할에 더 충실하기

상태 관리 패러다임이 등장한 이후, 한때 모든 서버 데이터 관리들을 Redux등으로 만드는 것이 유행했었죠. 그러나 이 과정은 컴포넌트가 비대해지는 것과 유사한 실수를 하게 됩니다.


서버 상태는 클라이언트와 달리 기본적으로 비동기와 예외를 항상 가지다 보니 다양한 문제들을 마주하게 됩니다. 가령:

  • 비동기 처리
  • 캐싱
  • 로딩 상태 관리
  • 재시도 메커니즘
  • 낙관적 업데이트
  • 페이지네이션
  • 무한 스크롤

이러한 기능들은 서버 상태를 관리할 때 필연적으로 다루게 되는 부분들이지만,
하나의 패러다임 안에 뭉뚱그리려다 보니, 의도와는 달리 비대해진 것입니다.
결국 모든 것을 하나의 기능이나 패러다임(즉, 상태 관리)으로 해결하려 하기보다는,
관심사를 더 세부적으로 나누어 계층을 분리하는 것이 효과적이었습니다.

이렇게 만들어진 현대 프론트엔드의 폴더구조


/api : 백엔드와의 통신을 담당하는 코드
/assets : 사용되는 에셋 파일
/components : 공통으로 쓰이는 컴포넌트
/config : 애플리케이션의 설정 파일들
/contents : 문서나 파일
/model : 데이터 구조를 정의하는 인터페이스나 타입
/hooks : 컴포넌트에서 사용되는 hook 파일들
/i18n : 다국어 지원을 위한 번역 파일들
/layout: 전체 애플리케이션의 구조를 정의하는 컴포넌트들
/libs : 공통으로 사용되는 라이브러리
/store : 상태 관리 로직과 관련 파일들
/service : 비즈니스 로직을 처리하는 서비스 레이어
/utils : 공통으로 사용되는 유틸리티 함수들
/pages : 실제 라우팅되는 페이지 컴포넌트들을 포함
/routes : 라우팅 설정과 관련된 코드
/server : 서버 관련 파일들
/styles : 전역 스타일이나 스타일 관련 설정
/types : 전역으로 사용되는 타입 정의
...

이렇게 여러 번의 시행착오를 거쳐 관심사의 분리를 통해 폴더 구조를 만들어내었습니다.
아마 여러분들도 이와 크게 다르지 않은 폴더 구조를 하고 있을 것입니다.
그렇다면 지금의 폴더 구조는 문제가 없는 걸까요?

반응형