프론트엔드 상태관리의 역사 - (1)

천재민(Karl)·2021년 11월 20일
20
post-thumbnail

들어가며


신입 프론트엔드 개발자로 일하면서 상태(state)상태관리(state management) 라는 말을 자주 들었습니다. 프론트엔드 개발자들은 자주 '이 기능은 상태를 더 추가해서 만들어야 할 것 같아요.', '불필요한 상태가 많은 것 같아요. 좀 더 줄여 관리할 수 없을까요?' 라고 말합니다. 그러나 '상태가 뭐고, 또 상태관리는 뭔가요?' 라는 질문에 명쾌하게 답할 수 있는 개발자들은 많지 않으리라 생각합니다. 저도 마찬가지이고요.

이런 찝찝한(?) 상태에서 벗어나고자 이 글을 쓰기로 했습니다. 이제 상태관리는 모던 프론트엔드를 이루는 핵심 요소입니다. 상태를 효과적으로 관리하고, 협업 시 상태에 대해 능숙하게 설명하고 전달하는 역량은 개발자의 핵심 역량이 되었습니다. 이러한 역량을 위한 투자는 전혀 아깝지 않을 것입니다!

기술을 깊이 이해하기 위해선 기술의 성립과 발전이 어떤 역사적 맥락에서 이루어졌는지 파악하는 것이 좋다고 생각합니다. 기술은 필요에 따라 만들어지기 때문입니다. 그렇기에 이 글은 시간적 흐름에 따라 상태관리의 발전 과정을 추적하는 방식으로 전개하도록 하겠습니다.


상태관리란


시간을 되짚어보기 전에 우선, 상태란 무엇인지 먼저 짚고 가겠습니다. 사실 상태는 갑자기 등장하여 프론트엔드 진영에서만 쓰이고 있는 개념이 아닙니다. 객체지향 프로그래밍(OOP, Object Oriented Programming)에선 객체가 변수로 자신의 상태를, 메소드로는 자신이 할 수 있는 행위를 갖습니다.


class Student {
  name;
  grade;
  major;

  constructor(name, grade, major) {
    this.name = name;
    this.grade = grade;
    this.major = major;

    // ... 다양한 상태들
  }

  speak() {
    console.log(
      `저는 ${this.major}를 전공하고 있는 ${this.grade}학년 ${this.name}이라고 합니다.`
    );
  }

  setName(newName) {
    this.name = newName;
  }

  setGrade(newAge) {
    this.age = newAge;
  }

  setMajor(newMajor) {
    this.major = newMajor;
  }

  // ... 다양한 행위들
}

이름과 학년, 그리고 전공 등의 상태를 지닌 간단한 Student 클래스입니다. 자신의 상태를 조합speak 메소드로 자신의 상태를 나타내는 문자열을 로깅하고 있습니다. 사용자는 브라우저 콘솔이나 터미널을 통해서 상태를 확인할 수 있는 것입니다.

학생이 이름과 전공을 바꾸는 것은 흔치 않지만 있을 수 있는 일입니다. setName, setMajor 와 같은 메소드들로 자신의 상태에 변화를 줍니다. 이렇게 변화된 상태speak 메소드의 문자열에도 반영되어 나타나겠죠?

Student 클래스 내에서 상태를 정의, 유지하고 변화를 줍니다. 그리고 외부에 상태를 반영합니다. 클래스 내에서 간단하지만 상태관리가 이뤄지고 있다고 할 수 있겠습니다.

이제 프론트엔드 쪽으로 좀 더 가까이 가볼까요?



저희 회사에서 서비스하고 있는 줌닷컴 모바일입니다. 저희 프론트엔드 파트에서 개발, 유지보수 하고 있죠.

많은 상태들이 있겠지만, 쉽게 파악할 수 있는 상태들은 다음과 같습니다.

  • 실시간 이슈 검색어 순위 (3. 김도훈 등 ...)
  • 서브도메인 탭 (뉴스, 랭킹, TV 연애 등 ...)
  • 서브도메인 컨텐츠 (멈추지 않은 홍준표 마이웨이 등 ...)

실시간 이슈 검색어 순위의 경우 클라이언트는 서버로부터 상태를 상대적으로 빠른 주기로 응답받아 유지, 변화시키고 사용자에게 제공합니다.

서브도메인 탭의 경우 사용자가 줌닷컴을 종료하거나 새로고침할 때까지 한번 받은 상태를 계속 유지하고 사용자에게 제공합니다. 물론 운영 단에서 서브도메인을 추가한 이후 새로고침을 하거나 종료 후 재접속한다면 이전과 다른 새로운 상태를 유지, 제공하겠죠.

서브도메인 컨텐츠의 경우 사용자와의 상호작용에 따라 상태를 변화시키고 제공합니다. 사용자가 선택하면 컨텐츠를 바꾸고 제공하는 거죠.

지금까지 살펴본 바에 따르면 사실 상태는 데이터입니다. OOP에서의 상태는 객체를 설명하며 가변적인 데이터이죠. 상태(데이터)없이 행위(메소드)로만 객체를 정의하려고 한다면 불충분할뿐더라 행위(메소드)도 제약적으로 정의될 수밖에 없습니다. 프론트엔드에서의 상태란 사용자에게 제공하고자하는 데이터입니다. 내부 로직을 위한 상태조차도 간접적으로 사용자에게 노출됩니다.

그렇다면 프론트엔드의 상태관리는 무엇일까요? Student 클래스의 speak 메소드를 기억하시나요? 지닌 상태를 잘 반영하고 있습니다. 상태의 변화도 물론 반영하겠죠. 메소드는 상태에 대한 함수입니다.

앞선 줌닷컴 모바일UI(User Interface)도 마찬가지입니다. 지닌 상태를 잘 반영하고 변화가 생기면 변화한 상태를 반영합니다. 그렇담 우리는 메소드와 마찬가지로 UI도 상태에 대한 함수라고 할 수 있지 않을까요?

결국, 정확한 정의라곤 할 순 없지만 우리는 프론트엔드의 상태관리를 목표한 UI를 - 좀 더 확장하면 UX(User Experience)까지 - 구현하기 위해 적절히 상태(데이터)와 그 구조를 정의, 설계하고 상태가 언제, 어떻게, 왜 변화했는지 잘 추적하는 일이라고 이해할 수 있겠습니다. 상태를 UI라는 함수에 적용했을 때, 문제를 일으키지 않도록 말이죠.

UI, UX를 상태에 대한 함수로 여기는 개념은 모던 프론트엔드 프레임워크들이 차용하고 있습니다.



Vue에선 반응성(reactivity)을 입힌 데이터를 상태라고 합니다. 이러한 상태에 변화가 생기면 watcher 인스턴스를 거쳐 렌더링 함수를 트리거하고 상태에 따라 UI를 렌더링합니다.


jQuery의 상태관리


프론트엔드 개발에 주로 jQuery가 사용되었을 때는 상태를 유지하기 위해 jQuery의 data()HTML5의 data- 속성을 사용했습니다.


<script src="http://code.jquery.com/jquery-3.6.0.js"></script>
<html>
	<head></head>
	<body>
		<span>상태를 유지하기 위한 span 태그!</span>	
		<script type="text/javascript">
			//data()을 사용하면 span의 jquery 객체에 name와 age 속성이 설정된다.
			$("span").data("name", "karl")
			$("span").data("age", "28")

			//혹은 data- 속성을 사용하면 span 엘리먼트에 data-name와 data-age 속성을 설정된다.
			var span = document.quertSelector("span")
			span.setAttribute("name", "karl")
			span.setAttribute("age", "28")
		</script>
	</body>
</html>

이 방법을 통해 상태를 DOM(Document Object Model)를 그릇 삼아 유지할 수 있었습니다. 당시 JavaScript(ES5, 6 이전)에선 상태를 격리하여 관리할 수 있는 선택지가 부족했고 그나마 있는 선택지들은 사용하기 어려웠습니다. window 객체를 사용하자니 전역오염이 걱정이 되었습니다. 그래서 JavaScript 보단 마크업에 유래하여 만들어진 DOM을 좀 더 베이스로 삼았고 DOM에 상태를 유지하는 것을 선호했습니다. 그러나 이 방법엔 문제가 있었습니다.

첫 째로, 상태에 대한 접근성이 떨어집니다. DOM상태를 저장하고 있으므로, 상태를 핸들링하기 위해선 DOM에 직접 접근해야 합니다. 원하는 상태를 어떤 DOM에 저장했는지 늘 추적해야 하고 상태를 사용하기 위해선 DOM에 직접 접근하는 코드를 늘 작성해야 합니다.

두 번째로, 상태의 파편화가 심하다는 것입니다. 상태는 구현하고자 하는 동작에 따라 여러 DOM에 흩어져 있습니다. 이는 버그가 발생했을 시 상태변화의 추적을 어렵게 합니다. 여러 흩어져 있는 DOM 관련 상태변화 로직에 console.log 를 일일이 작성하고 테스팅, 디버깅하는 자신을 생각해보세요. 살펴보아야할 로직이 많아 상태가 어디서, 언제, 왜, 어떻게 바뀌었는지 쉽게 파악할 수 없을 겁니다.

위 두가지 이유가 복합적으로 작용하여 상태관리의 복잡성을 키우곤 했습니다. 이는 개발과 확장, 유지보수를 어렵게합니다. 하지만 괜찮았습니다. 아직은 스마트폰의 시대가 아니었으니까요.


AngularJS의 상태관리


스마트폰의 기원은 2000년대 이전까지 거슬러 올라가 봐야 하지만, 스마트폰이 우리 시대에 본격적으로 등장한 때는 2007년 초 아이폰 출시부터입니다. 그리고 2010년 쯤에는 완전한 대세가 되었습니다.

웹의 활동 무대는 더 이상 PC에 한정되지 않았습니다. 수많은 웹이 스마트폰 위에서 돌아가기 시작했습니다. 스마트폰을 통해 웹은 이전보다 압도적으로 많은 사용자를 상대할 수 있었습니다.



자료를 보시면 폭증하는 모바일 점유율을 확인하실 수 있습니다.

폭증한 사용자들은 곧 늘어난 사업 기회, 고도화된 사업 모델을 뜻합니다. 웹은 이러한 요구에 발맞추어 웹앱이라고 부르는 복잡하고 정교한 웹으로 거듭나야 할 수밖에 없었습니다. 이전에 비해 코드량도 폭발적으로 증가하죠.

매우 강력한 기능들로 무장해 높은 사용자 경험(UX)을 선사해야 하는 웹앱의 상태관리는 이전과 비교할 수 없을 정도로 복잡해질 수밖에 없었습니다. 이젠 웹앱 환경에서 더 이상 jQuery의 상태관리로는 효과적인 개발과 확장, 유지보수를 보장할 수 없었습니다.

또한, SPA(Single Page Application)에 대한 요구도 있었습니다. 상태관리에 따른 잦은 페이지 새로고침은 모바일 환경에서 나쁜 사용자 경험을 주었습니다. 다행히 브라우저 성능이 이전에 비해 크게 개선되어 SPA를 시도할 수 있는 발판이 마련되었습니다. 이를 바탕으로 Backbone.js 와 같은 프레임워크들이 SPA를 시도하였으나, 아직 만족스러운 결과를 가져오진 못하고 있었습니다.

성숙해진 자바스크립트 생태계에서, 이러한 문제를 해결하고자 AngularJS가 등장했습니다. 아래는 AngularJS가 제공하는 튜토리얼 코드 중 일부입니다. 휴대폰 리스트를 렌더링하고 입력에 따라 검색과 정렬을 할 수 있습니다. 실제 작동은 여기서 확인하실 수 있습니다.


// app/index.html 파일
<html ng-app="phonecatApp">
	<head>
		<script src="js/controllers.js"></script>
	</head>
	<body ng-controller="PhoneListCtrl">
		Search: <input ng-model="query">
		Sort by:
		<select ng-model="orderProp">
		  <option value="name">Alphabetical</option>
		  <option value="age">Newest</option>
		</select>
		<ul class="phones">
		  <li ng-repeat="phone in phones | filter:query | orderBy:orderProp">
		    <span>{{phone.name}}</span>
		    <p>{{phone.snippet}}</p>
		  </li>
		</ul>
	</body>
</html>

ng- 로 시작하는 HTML 속성들이 눈에 띄실 겁니다. 이를 AngularJS에선 directive라고 부릅니다. 이 directive를 통해 정적 HTML에 자바스크립트 코드(그리고 AngularJS 문법에 따른 코드)를 바인딩함으로써 HTML을 동적으로 만드는 겁니다.

ng-controllerscope라고 부르는 영역을 생성합니다. 이 scope에 해당하는 마크업(<body></body>와 그 자식들) 은 controller의 자바스크립트 코드 동작을 따르게 됩니다.


// app/js/controllers.js

var phonecatApp = angular.module('phonecatApp', []);

phonecatApp.controller('PhoneListCtrl', function ($scope) {
  $scope.phones = [
    {'name': 'Nexus S',
     'snippet': 'Fast just got faster with Nexus S.',
     'age': 1},
    {'name': 'Motorola XOOM™ with Wi-Fi',
     'snippet': 'The Next, Next Generation tablet.',
     'age': 2},
    {'name': 'MOTOROLA XOOM™',
     'snippet': 'The Next, Next Generation tablet.',
     'age': 3}
  ];

  $scope.orderProp = 'age';
});

scopephones 라는 배열 형태의 상태(데이터)가 있습니다. AngularJSscope에 해당되는 마크업상태를 렌더링합니다. 사용자와의 상호작용으로 상태에 변화가 생기더라도 자동으로 변화사항을 마크업에 반영(DOM을 조작)하여 마크업과 상태의 싱크를 맞춥니다.



AngularJS상태관리를 DOM으로부터 독립시켜 자바스크립트의 품으로 돌려줬습니다. 이를 통해 앞서 언급한 jQuery 상태관리의 약점인 상태에 대한 낮은 접근성과 파편화를 어느 정도 해결해주었습니다.

자바스크립트로 작성된 컨트롤러 내에서 상태관리를 하게 되었습니다. 이는 더 이상 DOM에 접근하는 로직이 필요치 않다는 것을 말합니다. 또한, 여러 DOM에 흩어져 있던 상태들을 같은 컨트롤러 모아서 관리하니 핸들링하기 한층 쉬워졌습니다.

또한, 사용자에게 훌륭한 SPA 환경을 제공해 좋은 사용자 경험을 선사할 수 있게 되었죠.

하지만 AngularJS의 상태관리도 완벽한 것은 아니었습니다. '어느 정도' 라고 말씀드렸죠. 글이 길어졌네요. 다음 편에 이어서 말씀드리도록 하겠습니다.

궁금하신 점, 피드백, 기타 의견이 있다면 댓글로 남겨주세요! 댓글은 작성자에게 힘이 됩니다.

감사합니다!


기타


줌인터넷에서 블로그 스터디를 운영하고 있습니다.


참고문헌

https://junilhwang.github.io/TIL/Javascript/Design/Vanilla-JS-Store/#_1-%E1%84%8C%E1%85%AE%E1%86%BC%E1%84%8B%E1%85%A1%E1%86%BC-%E1%84%8C%E1%85%B5%E1%86%B8%E1%84%8C%E1%85%AE%E1%86%BC%E1%84%89%E1%85%B5%E1%86%A8-%E1%84%89%E1%85%A1%E1%86%BC%E1%84%90%E1%85%A2%E1%84%80%E1%85%AA%E1%86%AB%E1%84%85%E1%85%B5

https://www.youtube.com/watch?v=o4meZ7MRd5o

https://docs.angularjs.org/tutorial/step_02

https://kr.vuejs.org/v2/guide/reactivity.html

https://gs.statcounter.com/

https://violetboralee.medium.com/jquery-object-20094ebac68d

https://www.nextree.co.kr/p10155/

https://backbonejs.org/

https://medium.com/wematch/프론트엔드의-상태관리란-무엇인가-5ff888dab7ad

profile
나에게 나무를 자를 여섯 시간을 준다면, 나는 먼저 네 시간을 도끼를 날카롭게 하는 데에 쓰겠다.

0개의 댓글