프론트엔드 개발을 하다 보면 컴포넌트를 어떻게 설계하는지에 따라 코드의 재사용성, 유지보수성, 그리고 가독성이 크게 달라지는데요.
이 글에서는 선언적 코딩이 나오게 된 배경과 제 생각과 실제로 React로 선언적인 코딩을 하는 방법에 대해서 얘기 나눌 예정입니다.
선언적
이라는것은 뭘까요?
선언적
인 방법이 있기전에는 개발자들은 어떻게 코딩했을까요?
"Declarative"는 라틴어 declarare에서 유래했는데, 이 단어는 de-("완전히") + clarare("명확하게 하다")로 구성돼있습니다. 즉, **"어떤 의도를 명확히 표현하다"**는 의미입니다.
여기서 중요한 단어는 의도인데요.
사실 우리가 일상적으로 사용하는 모든 언어는 기본적으로 선언적입니다.
우리는 친구에게 밥 먹자고 말할 때, 그저 "밥 먹자!" 라는 의도를 표현할 뿐이지,
"의자에서 일어나 신발을 신고 밖으로 나가 음식점으로 이동한 뒤 메뉴판을 보고 음식을 주문해서 함께 먹자"고 일일이 설명하지 않습니다. 왜냐하면 그 방법은 이미 충분히 공유된 맥락(context) 속에 존재합니다.
하지만 프로그래밍은 오랫동안 "공유된 맥락"이 존재하지 않는 세상이었다고 합니다.
"무엇을" 하겠다는 의도보다는 "어떻게" 하겠다는 방법이 훨씬 중요했던 시절이 있었습니다.
그런데 최근에는 점점 더 많은 프로그래밍 언어와 프레임워크들이
마치 인간이 자연스럽게 의도를 표현하듯이, 코드 자체가 "공유된 맥락"을 가지고 개발자의 "의도"를 직접 담아내려 하고 있습니다.
이런 맥락에서 선언적 프로그래밍이란,
기계(컴퓨터)가 인간의 의도를 더 잘 이해하도록 설계된 방식이며,
우리가 선언한 무엇이 하나의 명확한 "메시지"가 되는 방식이라고 볼 수 있습니다.
React와 SwiftUI 같은 선언형 프레임워크가 개발자들에게 크게 환영받는 이유도 여기에 있습니다.
개발자는 코드가 "어떻게 동작하는지" 일일이 설명하지 않고도,
자신이 만든 컴포넌트가 무엇을 의미하는지, 즉 자신의 의도를 드러내서 코딩하는 방식을 활용합니다.
그럼 프론트엔드에서 선언적 코딩은 언제부터 관심을 받기 시작했을까요?
웹은 처음엔 인터넷에서 문서를 보여주기 위해서 사용됐습니다.
그래서 HTML은 문서를 웹에서 보여주기 위한 용도로 사용됐고 이에 스타일을 효율적으로 입혀주기 위해 CSS가 나옵니다.
그렇게 웹이 발전하면서 문서를 동적으로 수정하고자 하는 니즈가 생겼고 이를 위해 Javascript가 2주만에 개발됩니다.
그렇게 웹은 이제 단순한 문서가 아니라 동적으로 내부를 수정하면서, 외부에 HTTP요청으로 데이터를 받고, 클라이언트에서 만들어낸 데이터를 관리하여 이로 문서를 조작하는 등 더 복잡해지게(다양한 작업을 다룰 수 있게) 됩니다.
그러면서 당연하게도 DOM을 동적으로 다루는 Javascript의 필요성과 사용량이 늘어나게 됩니다.
아래 사진을 참고하면 2011년 평균 자바스크립트 전송량이 88.4kb에서 현재 642.4kb에 이르게 됩니다.
위에서 봤던것 처럼 웹은 더 복잡해지면서 더 많은 일들을 하게 됩니다.
당연하게도 많은 자바스크립트가 있다는 뜻은 소프트웨어에 담긴 코드나 기능들이 더 복잡해졌다는 뜻이고, 소프트웨어가 복잡해질수록 새로운 아키텍처나 디자인 패턴과 같은 방법론이 대두됩니다.
Facebook도 이런 문제를 갖고 있었고, React가 개발됩니다. Facebook이 React를 개발한 이유는 크게 세가지 였습니다.
이 글의 목적에 맞게 1번에 대해서 좀 더 짚어보고자 합니다. 실제로 Facebook이 React를 개발한것도 늘어나는 인원과 코드 관리에 대해서 어려움을 겪었던 이유가 가장 컸기 때문이라고 공식 블로그글에 나와 있습니다.
React로 선언적 코드를 작성하는 예시에 대해서 공유드리며 어떻게 유지보수성이 좋아졌나에 대해서 설명해보려 합니다.
예를들어 버튼을 클릭하면 숫자가 증가하는 Count컴포넌트를 만든다라고 했을때 바닐라로 개발하는것과, React로 개발하는 코드를 비교해보면 알 수 있습니다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<title>Imperative Counter</title>
<style>
#imperative-counter {
font-family: sans-serif;
}
button {
margin-top: 8px;
}
</style>
</head>
<body>
<div id="imperative-counter"></div>
<script>
const container = document.getElementById('imperative-counter');
let count = 0;
const countText = document.createElement('p');
countText.textContent = `Count: ${count}`;
container.appendChild(countText);
const button = document.createElement('button');
button.textContent = 'Increment';
button.onclick = () => {
count += 1;
countText.textContent = `Count: ${count}`;
};
container.appendChild(button);
</script>
</body>
</html>
// DeclarativeCounter.tsx
import { useState } from 'react';
export default function DeclarativeCounter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount((prev) => prev + 1)}>Increment</button>
</div>
);
}
순수 Javascript로 개발하면 어떻게에 집중합니다.
위 코드처럼 getElementById method로 특정 요소를 잡고(어떻게 시작할지 정의) 이를 CountText를 직접 해당 요소안에 생성해주고, 버튼 요소를 만들어서 onclick이벤트를 심어줍니다.
해당 코드는 적절히 잘 만들어진 코드이긴 하지만 어떤 동작을 하는지 한눈에 잘 들어나지 않습니다.
예를들어 이 코드를 코딩을 배우고 싶은 후배에게 알려준다고 한다면 DOM이 무엇이며, appendChild와 getElementById와 같은 method에 대해서 설명이 먼저 필요할것입니다.
하지만 React로 개발하면 단순합니다.
내가 어떤 상태로 해당 컴포넌트의 렌더주기를 관리할것인지 선언
합니다.
위 코드에서는 count라는 상태를 갖고 있으며 count라는 상태를 변형할 수 있는 setCount함수를 같이 제공합니다.
어떤 값을 변형시킬것이고 어떻게 DOM에 추가해줄것인지 정의했던 기존 코드와는 달리 의도를 전달함으로써 기능을 정의합니다.
상태를 변형시키면 React 내부 상태머신에 맞게 값이 변형되고 렌더트리를 조정해서 한번에 적용(commit)합니다.
그래서 위 코드보다 아래코드가 위에서 아래로 읽어나갈때 가독성이 더 좋습니다. 이는 실제로 유지보수 과정에서 엄청난 이점을 제공합니다.
React 코드가 이처럼 간단하다고 실제로 간단한 동작은 아닙니다.
리액트 코드들은 컴파일돼 리액트 상태머신에 맞게 관리될 수 있는 거대한 코드속으로 포함됩니다.
하지만 선언적인 인터페이스를 제공함으로써 실제로 내부적으로 어떻게 되는것은 React에서 처리하고 개발자에게는 코드에 대한 의도만 포함하게 합니다.
선언형 코딩을 해야하는 이유에 대해 좀 더 잘 알기위해 클린아키텍처에 대해서 설명하려합니다.
이것이 선행돼야 우리가 왜 선언형으로 코딩을 작성하는 노력을 해야하는지 납득되기 때문입니다.
클린아키텍처 관점에서 보면 프론트엔드는 세부사항입니다.(아래 그림의 제일 바깥쪽 파란 테두리에 Web, UI가 있는걸 확인할 수 있습니다.) 안으로 갈수록 잘 바뀌지 않는 성질을 갖고 있고, 바깥으로 갈수록 자주 바뀌는 성질을 갖고 있습니다.
그래서 클린아키텍처에서는 성질에 대한 레이어를 구분하고 바깥에서 안쪽으로 참조하는 방향(단방향)으로 Software를 설계할것을 강조합니다.
왜냐하면 서비스에서 중요한 Entities와 같은것(Ex: 우리 회사의 핵심가치)은 잘 바뀌면 안되는것들입니다. 대부분의 변경사항은 이런 잘 안바뀌는것들을 조합해서 잘 바뀔 수 있는 서비스의 클라이언트 부분을 수정하거나 추가합니다.
그럴 수 밖에 없는게 Server에 대한 구현부는 유저에게 직접 드러나지 않으므로 상대적으로 더디고 유저가 직접 맞닿은 Frontend는 UI(User Interface)로 구성돼있기에 그렇습니다.
디자인이 안맞거나 요청중에 대한 로딩상태를 표현해주거나 입력이 틀렸을때 화면에 나타내주지 않으면 이는 직접적인 유저 경험과도 이어집니다.
Frontend개발자는 클라이언트 즉 유저가 사용하는 GUI를 개발하기 때문입니다.
그리고 선언적 코딩을 하려는 시도는 현대 프론트엔드 뿐만 아니라 운영체제의 GUI를 개발하던때부터 쭉 나오던 얘기라고합니다.
그리고 앱 개발 프레임워크에서도 선언형 코딩에 대해서 강조합니다.(Flutter, IOS, Android 등)
즉 이를 정리하면 프론트엔드 개발자 또한 GUI(웹)을 개발하기에 선언형 코딩에 대해서 강조할 필요가 있다라는 결론을 낼 수 있습니다.
IOS에서 개발한 SwiftUI에서도 Declare the user interface
로 시작하네요!
우리는 프론트엔드 개발자이기 전에 Software Engineer입니다. Software Engineer는 우리가 만드는 Software를 Soft하게 관리 할 필요가 있습니다.
Hardware: "컴퓨터가 따르는 명령어(Software)가 아니라, 컴퓨터의 물리적이고 전자적인 부품들"
Software: 소프트웨어는 '부드러운'이라는 뜻을 가진 소프트(soft)와 '제품'이라는 뜻을 가진 웨어(ware)라는 단어가 복합되어 이루어진 단어
Software은 Hardware와는 다릅니다. 말 그대로 Hardware에 비해 부드럽습니다. Software와 Hardware의 명명의 이유는 변경가능성 이라고 생각합니다.
예를들어 우리가 CPU, CPU, RAM, SDD와 같은 Hardware장치들로 컴퓨터를 한번 구매하면 새로운 부품을 사지 않는이상 해당 기기들의 변형이 불가능합니다. 특정 Hardware의 기기의 클럭을 조금 늘리는 약간의 오버클럭정도만 가능할 뿐입니다.
하지만 우리가 만드는 Software는 다릅니다. Software는 한번 만들어지면 끊임없이 수정하고 기능이 추가됩니다. 우리가 사는 세상의 디지털세계는 끊임없이 변하는것이 디폴트이기 때문입니다.
만약 Software를 Hardware처럼 만든다면 아래 짤과 같은 사람들이 많아지고 우리 서비스를 이용하는 사람들은 대부분 떠나갈것입니다.
이처럼 Software는 애초에 변경을 해야하는 성질을 갖고 있고 Software Engineer는 제품을 soft하게 만들어야 할 책무가 있습니다. - Clean Architecture에서 -
정리하면 Frontend 개발자가 만드는 Software는 태생적으로 변경이 필요하도록 태어났습니다. 그래서 우리는 좋은 Software를 만들기 위해서는 명령형으로 코딩하고 변경에 어렵도록 작성하는 것 보다는, 선언형으로 코딩하고 변경에 유리하도록 작성하여 더 많은 유저들에게 오랫동안 제공 할 수 있는 서비스를 만들어야 합니다.
개인적으로 느끼는 감정이긴 하지만, 유독 프론트엔드 개발 진영에서 선언적인 코딩에 관심이 많다고 생각합니다. 그리고 이는 많은 프론트엔드 개발자들이 React로 주로 개발하고 React의 철학이 선언적인 코딩을 할 수 있다이니 이 영향도 없지않아 있다라고 생각합니다.
백엔드와 비교해보면 백엔드는 시스템 전체의 안정성을 고민해야 하는 영역이지만, 오늘날에는 성숙한 프레임워크와 인프라 계층이 그 복잡도를 상당히 흡수해주기 때문에 개발자는 더 높은 레벨의 도메인과 흐름에 집중할 수 있지 않나 생각합니다.
클린아키텍처에서 얘기하듯이 프론트엔드는 세부사항이고 대부분 세부사항을 개발하는 일이 많습니다.
하지만 그럼에도 불구하고 우리가 만드는 제품은 Software이기에 유지보수와 기능추가에 유리해야합니다.
그래서 우리가 신경써야 할 서비스의 안정성은, 변경이 잦은 일이 많으니 이를 잘 관리하는것이 우리가 해야 할 핵심 업무였고 그렇다보니 프론트엔드 개발자들이 선언적 코딩에 관심이 많지 않을까? 라고 생각합니다.
우리는 프론트엔드, 즉 소프트웨어를 만들어냅니다.
소프트웨어는 수많은 사용자에게 도달하고, 그들의 사용 방식에 따라 끊임없이 변화할 수 있도록 설계되어야 합니다.
그래서 이 글에서는 그런 유연한 변화를 수용하기 위한 하나의 방법으로 선언형 코딩을 이야기해봤습니다.
하지만 선언형 코딩이 항상 정답인 것은 아닙니다.
명령형과 선언형은 둘 중 하나를 무조건 선택해야 하는 Boolean이 아니라, 문제의 성격에 따라 선택할 수 있는 도구입니다.
예를 들어, 제어 흐름이나 상태 변화가 복잡하게 얽혀 있을 때는 오히려 명령형 코딩이 더 명확하고 효과적일 수 있습니다.
저는 이렇게도 생각합니다.
우리는 선언형으로 "무엇을" 해결할지를 빠르게 표현하고, 그 "어떻게"에 대한 구현은 프레임워크에 위임하는 방향으로 점점 발전해왔습니다.
프레임워크가 제공하는 선언형 인터페이스 그리고 우리가 선언형으로 코딩하려는것은 더 많은 제약을 주기도 하지만, 그만큼 생산성과 안정성을 끌어올리는 데 큰 기여를 해주고 있습니다.
선언형에 대해서 많은 얘기를 나눴는데 다음에는 실제로 React로 개발할때 이런 방식으로 제어/비제어 그리고 실제 비즈니스에서는 어떻게 다루면 좋을지에 대해서를 다루는 글인 두번째 레슨으로 찾아뵙겠습니다.
좋은 내용 감사합니다!