최근 React로 개인 프로젝트를 구현하다가 JS 기초 개념이 부족한 것 같아 VanilaJS를 통한 새 협업 프로젝트를 하고 있다. React를 활용하면 Redux, Context를 통해 심플하게 state 관리가 가능한데 VanilaJS에서는 React의 최대 장점인 useState기능이 없어진 탓에 부모, 자식 객체 간의 깔끔한 상태 관리에 대해 다시금 깨닫고 있다..
이를 VanilaJS로, 그것도 clean code 형태로 구현하자니 어떻게 변수를 만들어 재사용할지, 또 어떤 로직이 효율적인 방법인지 떠올리기가 생각보다 쉽지가 않았다.
프로젝트 설계 전 전체적인 상태 구조를 짜지 않고 시작하게 되면 프로젝트가 복잡해질수록 유지보수가 더 힘겨워진다.
이번 포스팅은 프런트엔드 개발자가 갖춰야 할 상태 관리 개념에 대한 이해와 실제 적용 방법들을 되짚어보려 한다.
상태(state)란?
상태는 한국어로 번역하면 잘 와닿지 않는다. 대신 '데이터'라는 말이 더 이해하기 쉬울 것이다.
객체 지향 프로그래밍에서는 프로그램의 기본 단위가 객체이고, 객체 간의 메시지 상호작용을 통해 프로그램을 구현하게 된다. 이때 객체가 가지고 있는 데이터 또한 상태라고 할 수 있다.
예를 들어 Car 클래스를 만든다고 가정하면 자동차의 상태는 color와 type이 된다.
상태에서 중요한 부분은 바로 값이 변한다는 것이다.
이 값이 변하는데 예측된 값으로 변해야 의도한대로 동작을 수행할 수 있으니, 예측 범위 안에서 변하게 하는 것이 중요하다. 그래서 그 예측된 범위를 만들기 위해서, 여러 가지 제약조건이나 구조적인 설계를 고민할 수 있을 것이다.
이 예측 범위를 최소화 하기 위해서는
상태에 대해 일관되게 READ하는 로직과,
최소화한 Write하는 로직을 만들 때 가능하다.
범위를 최소화하는 건 모듈 단위, 스코프 단위에 대한 고민으로 확장될 수 있다. 상태에 대한 일관성은 상태를 여러 곳에서 같은 방식으로 READ 하는 방식을 고민해볼 수 있다. 상태를 여러 곳에서 직접적으로 수정한다면, 상태를 예측하고 관리하기 어려우니 수정하는 부분을 제한하고, 그 책임을 가지고 있는 대상이 분명하게 역할을 가진다면 최소화한 write가 가능해진다.
만약 인스타그램 화면을 구현한다면 어떤 상태가 필요할까?
- 내가 올린 사진들
- 게시물 수
- 팔로워 수
- 팔로잉 수
- 새로운 알림
- 이름, 닉네임
- 내가 현재 로그인이 되어있는지 여부
- 내가 태그된 게시물
- 데이터들이 서버로부터 로딩이 완료되었는지 여부
- 화면 내에 특정 UI를 노출할지 여부
- 현재 보고 있는 페이지
- 로그인한 유저의 권한
- text input의 입력 값
- checkbox input의 선택 여부
이 모든 것은 화면 구성에 필요한 데이터이고 이를 상태(state)라고 부른다.
상태 관리란?
상태 관리 개념은 프론트 엔드에서 자주 사용되며 백엔드 개발자에게는 다소 생소할 수 있다.
예를 들면 데이터에 맞춰 적절하게 UX와 UI를 설계하고 구현하는 것이 바로 상태 관리의 일종이다.
- 데이터가 변할 때마다 데이터에 관련된 dom을 일일이 찾아서 조작하고 싶지 않다면
- 전체 데이터의 형태와 리스트를 한 곳에서 효율적으로 관리하고 싶다면
프론트엔드 개발자는 구현 시 상태 관리에 대해 깊게 고민해봐야 한다.
1. 팔로워
59는 숫자의 길이가 2글자 밖에 안되므로 표기하는데 전혀 문제 되지 않는다. 하지만 2.1억을 210000000이라고 표기하는 것은 읽기도 힘들고 너무 길어서 UI가 이상해진다. 그럴 바엔 아래 스크린숏처럼 2.1억이라고 표기하는 게 더 짧고 읽기 쉽고 UI에 들어맞는다.
팔로워 상태에 따라 UI가 동적으로 변한다. 팔로워 수 표기의 상태관리다.
2. 게시물
게시물 리스트는 모두 비슷한 모양을 하고 있지만, 사진이 여러 장인지, 동영상인지 여부에 따라 아이콘을 각각 다르게 보여준다. 이는 컴포넌트를 만들어두고, 게시물 상태에 따라 UI가 다르게 반응한 경우이다.
3. 댓글
내가 올린 게시글에 새로운 댓글이 달렸다고 실시간으로 알림이 온 경우에도 상태 관리가 이루어진다.
(이는 실시간으로 상태가 관리되어야 하므로 훨씬 복잡해진다.)
4. 로딩 상태
인스타 피드에 접속할 때 찰나의 순간을 말한다. 게시물 정보를 다 불러오기 전에 검은색 박스 UI만 노출된다. 이런 UI를 Skeleton UI라고 부르며, 용량이 큰 데이터나 사진 등의 정보를 로딩할 때 사용자에게 피드백을 주기 위해 사용한다.
5. 에러 예외 상황
인터넷 연결이 끊겼을 때 빨간색 알림 영역이 생기고, ‘인터넷 연결 없음’이라는 문자가 표시된다. 개발 시 예외처리는 매우 중요하다. 상태 관리는 예외적으로 발생할 수 있는 경우를 고려해서 적절한 피드백을 주기 위해 사용하기도 한다.
프론트엔드 개발자로서의 시사점
- 프론트엔드에서 상태관리는 한 곳에서 하면 효율적이다.
- 보통은 최상위 컴포넌트나 storage를 따로 두어 관리한다.
- 자식 컴포넌트들은 최상위 상태를 가지고 있는 개체로부터 상태를 받아 render 해주는 역할을 한다.
최근 프론트엔드 개발의 기본 단위는 재사용 가능한 컴포넌트이다. 이 컴포넌트 간의 상태의 공유 여부로 지역적 상태(local state) 또는 전역적 상태(global state)를 적절히 구분해서 설계해야 한다. 또한 상태는 일관적인 형태를 유지해서 무결성을 지켜야 한다. 예를 들어서 팔로워 수를 표시하는 화면이 3군데 있다고 가정해보자. 상태가 일관적이지 않으면 3 화면 다 다른 팔로워 수를 표시할 수 있다. 팔로워가 실시간으로 증가했으면 3화면 다 같은 데이터를 보여주어야 한다.
또한 상태를 어디에 어떻게 저장하고 사용할 건지도 중요하다. 상태 공유 여부에 따라 전역적으로 redux나 vuex 같은 스토어에 저장하던지, 브라우저가 꺼져도 상태가 유지되도록 브라우저의 쿠키(Cookie) 또는 로컬 스토리지(localStorage)에 저장하는 것도 고려할 수 있다. 심지어 url의 상태 값을 포함시켜서 관리도 가능하다. 백엔드와 실시간 소통이 중요하다면 웹소켓으로 관리할 수도 있다.
그리고 상태와 상태의 조합에 따른 경우의 수가 굉장히 많다. 예를 들어서 0과 1의 이진법 2자리 상태가 있으면 총상태는 00, 01, 10, 11로 4가지가 된다. 이렇게 조합으로 나올 수 있는 경우의 수를 다 고려하고 우선순위를 설정해서 UI에 반영하는 작업 또한 매우 중요한 작업이다.
setState & render를 통한 상태 관리
TodoApp을 구현한다고 가정해보자.
input창에 todoItem을 새로 추가한다고 했을 때, 그 추가하는 메서드를 부모 컴포넌트에서 관리하고, 자식 컴포넌트는 부모 컴포넌트가 데이터를 직접 다루는 메서드를 사용하기만 하는 것이다.
// 부모 컴포넌트
function TodoApp() {
this.todoItems = [];
this.setState = updatedItems => {
this.todoItems = updatedItems;
todoList.setState(this.todoItems);
};
new TodoInput({
onAdd: contents => {
const newTodoItem = new TodoItem(contents);
this.todoItems.push(newTodoItem);
this.setState(this.todoItems);
}
});
}
// 입력 받는 컴포넌트
function TodoInput({ onAdd }) {
const $todoInput = document.querySelector("#new-todo-title");
$todoInput.addEventListener("keydown", event => this.addTodoItem(event));
this.addTodoItem = event => {
const $newTodoTarget = event.target;
if (this.isValid(event, $newTodoTarget.value)) {
onAdd($newTodoTarget.value);
$newTodoTarget.value = "";
}
};
}
// todoList 보여주는 컴포넌트
function TodoList() {
this.setState = updatedTodoItems => {
this.todoItems = updatedTodoItems;
this.render(this.todoItems);
};
this.render = items => {
const template = items.map(todoItemTemplate);
this.$todoList.innerHTML = template.join("");
};
}
이렇게 하면, 데이터를 한 곳에서 효율적으로 관리할 수 있고 데이터가 변경되었을 때 렌더링을 새로 해주는 로직도 재사용할 수 있다.
참고링크
'Front-End' 카테고리의 다른 글
Emotion으로 React 컴포넌트 디자인하기 (0) | 2022.10.06 |
---|---|
이벤트(Event) 흐름 제어하는 법 (0) | 2022.02.11 |
BOM (Browser Object Model) 완벽 정복하기 (0) | 2022.02.09 |
DOM (Document object Model) 완벽 정복하기 (0) | 2022.02.08 |
SaaS (Software as a service) (0) | 2020.03.12 |