선언형 프로그래밍(Declarative Programming)
React는 상호작용이 많은 UI를 만들 때 생기는 어려움을 줄여줍니다. 애플리케이션의 각 상태에 대한 간단한 뷰만 설계하세요. 그럼 React는 데이터가 변경됨에 따라 적절한 컴포넌트만 효율적으로 갱신하고 렌더링합니다.
선언형 뷰는 코드를 예측 가능하고 디버그하기 쉽게 만들어 줍니다.
- React(https://ko.legacy.reactjs.org/)SwiftUI uses a declarative syntax, so you can simply state what your user interface should do. For example, you can write that you want a list of items consisting of text fields, then describe alignment, font, and color for each field. Your code is simpler and easier to read than ever before, saving you time and maintenance.
- SwiftUI(https://developer.apple.com/xcode/swiftui/)Jetpack Compose는 Android를 위한 현대적인 선언형 UI 도구 키트입니다. Compose는 프런트엔드 뷰를 명령형으로 변형하지 않고도 앱 UI를 렌더링할 수 있게 하는 선언형 API를 제공하여 앱 UI를 더 쉽게 작성하고 유지관리할 수 있도록 지원합니다. 이 용어에 관해 몇 가지 설명이 필요하며, 앱 디자인에 있어 중요한 함의를 갖습니다.
- Android studio(https://developer.android.com/jetpack/compose/mental-model?hl=ko)
2019년 2월 페이스북, React 16.8.0 버전에서 함수 컴포넌트만을 이용해 React를 사용할 수 있도록 Hook 기능 추가.
2019년 6월 애플, UI 개발을 위한 프레임워크 Swift UI 추가.
2020년 1월 구글, 안드로이드 네이티브 UI를 작성하기 위한 새로운 도구 Jetpack Compose 0.1.0-dev04 버전 공개.
왜 UI 프레임 워크들은 선언적으로 변하고 있을까? 이 질문에 답하기 전에 이 글에서 선언형 프로그래밍의 개념과 예시에 대해 알아보자.
선언형 프로그래밍이란?
선언형 프로그래밍이란 원하는 결과가 무엇인지(WHAT)에 초점을 맞춰 표현하는 것이다. 이는 코드의 가독성을 높이고 유지보수를 용이하게 할 수 있는 장점이 있다. 왜냐하면 개발자는 코드의 복잡한 내부 로직에 대해서는 신경 쓰지 않고 원하는 결과에만 집중 할 수 있기 때문이다.
근데 여기서 궁금한 점이 생겼다. 내부 로직에 관한 코드가 없는데 어떻게 전과 똑같이 실행될 수 있을까? 내부 로직은 어디로 사라진걸까?
선언형 프로그래밍 vs 명령형 프로그래밍
사실 내부 로직에 관한 코드는 사라진게 아니라 숨겨져 있다. 내부 로직이 작성된 프로그래밍을 명령형 프로그래밍이라고 할 수 있는데 이는 작업을 수행하는 방법(HOW)에 초점을 맞춰 과정을 단계별로 표현하는 것을 말한다. 선언형 코드는 명령형 코드에서 복잡한 내부 로직(과정)을 감추고 결과만 노출하는 추상화(일종의 리팩토링)이라고 할 수 있다.
위의 정의만 보면 선언형과 명령형이 어떤 것인지 이해되지 않을 수 있다. 이를 조금 더 쉽게 이해하기 위해 실생활의 예시부터 한 번 살펴보자.
식당
명령형(HOW): 저기 창가 쪽 테이블이 비어 있네요. 저의 일행들은 저 테이블에 걸어가 앉을 예정입니다.
선언형(WHAT): 4명 테이블 부탁합니다.
위의 예시에서 알 수 있듯 명령형은 식당에 가서 어떻게 자리를 잡을 것인지에 대해 초점이 더 맞춰져있다. 단계별(걸어가 -> 앉음)로 작업의 형태를 표현하고 있다. 선언형의 경우 무엇을 원하는지에 대해 더 초점이 맞춰져있다. 즉, 4명이 앉을 수 있는 자리에 더 집중하고 있는 것이다.
길찾기
다른 예시를 보기 전에 질문을 하나 할건데 명령형과 선언형에 대한 답을 모두 생각해보자.
"나는 지금 월마트 바로 옆에 있어. 너희집까지 어떻게 가야 해?"
명령형(HOW): 주차장 북쪽 출구로 나와서 좌회전 해. 북쪽으로 가는 I-15를 타면 12번가 출구가 나와. 이케아 가는 것처럼 출구에서 우회전 해. 그 다음 직진하여 첫 번째 신호등에서 우회전해. 직진한 뒤 첫 번째 신호등에서 우회전하고 다음 신호등에서 좌회전하세요. 우리 집은 #298이야.
선언형(WHAT): 내 주소는 298 West Immutable Alley, Eden, Utah 84310야.
수동 변속기 차량과 자동 변속기 차량
명령형(HOW): 수동 변속기(1종) 차량
선언형(WHAT): 자동 변속기(2종) 차량
위의 예시에서 명령형 방식에서 선언형 방식으로 추상화 된 것은 무엇일까?
식당 직원이 손님을 테이블에 데리고 가는 방법을 알고 있다고 가정
주소만 알면 집을 찾아가는 방법을 알고 있는 어플이 있다고 가정
자동 변속기(2종) 차량은 변속 기어 위에 추상화 층이 있음
이제 실생활을 벗어나 코드를 통해 선언형 프로그래밍과 명령형 프로그래밍에 대해 알아보자.
선언형 프로그래밍과 명령형 프로그래밍의 예시
JavaScript 메서드
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const evenNumbers = [];
for (let i = 0; i < numbers.length; i++) {
if (numbers[i] % 2 === 0) {
evenNumbers.push(numbers[i]);
};
};
console.log(evenNumbers);
// [2, 4, 6, 8, 10]
위 JS 코드는 "인덱스 0부터 numbers 배열의 길이만큼 반복해서 해당 인덱스의 숫자가 짝수 일 경우 evenNumbers 배열에 push한다." 라는 문제 해결 과정을 담고있다. 사실 이 코드에서 중요한 것은 짝수 숫자를 필터링 하는 것이다. 이 코드를 해석하기 위해서는 중요하지 않은 과정 또한 함께 확인해야 하기에 가독성이 저하되고 유지보수 하기도 힘들다. 이런 과정을 숨기는 방법은 없을까?
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const evenNumbers = numbers.filter(number => number & 2 === 0);
console.log(evenNumbers);
// [2, 4, 6, 8, 10]
JS 내장 메서드인 filter()
를 사용하면 된다. 이 메서드를 사용하면 문제 해결 과정은 JS 내부 로직에 맡겨두고 [짝수 숫자를 필터링] 한 결과에만 초점을 맞춘 코드를 작성할 수 있다. 이렇게 코드를 작성하면 전체적인 가독성을 높여 개발자가 본질에만 집중할 수 있게 만든다.
React
$("#btn").click(function() {
$(this).toggleClass("highlight")
$(this).text() === 'Add Highlight'
? $(this).text('Remove Highlight')
: $(this).text('Add Highlight')
})
위 jQuery 코드는 다음과 같은 과정을 담고 있다. "btn의 ID를 가진 요소에 클릭 이벤트 핸들러를 추가한다. 클릭하면 highlight 클래스를 토글(추가 또는 제거)하고 요소의 현재 상태에 따라 텍스트를 Remove Highlight 또는 Add Highlight로 변경한다."
<Btn
onToggleHighlight={handleToggle}
highlight={highlight}>
{buttonText}
</Btn>
하지만 위의 React 코드의 경우 Btn 이라는 컴포넌트 내에 jQuery에서의 과정을 추상화해 상태보다 어떤 UI가 보일지 설명하는데에 집중한다.
React Concurrent UI 패턴
const Component = () => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState();
const [data, setData] = useState();
useEffect(() => {
async () => {
try {
setIsLoading(true);
const { json } = await fetch(URL);
setData(json());
setIsLoading(false);
} catch(e) {
setError(e);
setIsLoading(false);
}
}();
}, []);
if (isLoading) {
return <Spinner />
}
if (error) {
return <ErrorMessage error={error} />
}
return <DataView data={data} />;
}
export default Component;
위의 컴포넌트는 로딩 여부와 에러 여부를 가지고 있고 로딩중 일 때와 에러가 발생했을 때 각각 Spinner 컴포넌트와 ErrorMessage 컴포넌트를 노출해주고 그렇지 않을 경우 DataView 라는 컴포넌트를 노출해주고 있다. 여기서 컴포넌트는 조건부 렌더링을 사용하고 있는데 상태가 로딩 여부와 에러 여부만 가지고 있다면 비교적 단순한 코드라 할 수 있지만 더 많은 비즈니스 로직 핸들링이 추가된다면 결과물을 예측하기 어려울 수 있다.
그럼 위의 코드는 어떻게 선언형으로 개선할 수 있을까? React 18 버전에서 Suspense, Error Boundary 를 활용해 Concurrent UI 패턴을 사용하면 무엇을 보여주고 싶은지에만 집중해서 코드를 작성할 수 있다.
const Component = () => {
const [data, setData] = useState();
useEffect(() => {
async () => {
try {
setIsLoading(true);
const { json } = await fetch(URL);
setData(json());
setIsLoading(false);
} catch(e) {
setError(e);
setIsLoading(false);
}
}();
}, []);
return <DataView data={data} />;
}
const App = () => {
return (
<ErrorBoundary FallbackComponent={ErrorMessage}>
<Suspense fallback={<Spinner />}>
<Component />
</Suspense>
</ErrorBoundary>
)
}
Error Boundary와 Suspense를 통해 조건부 렌더링 부분의 제어권을 부모 컴포넌트인 App에 맡겨 처리하고 있다. 이렇게 Concurrent UI 패턴을 사용하면 어떤 것을 보여줄지에 대해서만 집중하기 때문에 이해하기 쉬운 UI를 작성할 수 있다.
마치며
그렇다면 이 글의 부제인 'React, SwiftUI, Android studio는 왜 [선언형 UI]를 사용하는 걸까?'라는 질문에 대한 답은 어떻게 내릴 것인가? 필자 개인적인 견해로는 DX(Developer Experience)를 위해서라고 생각한다. 아무리 뛰어난 프레임워크나 라이브러리라도 개발 경험이 개발자에게 가장 큰 영향을 미칠 것이기 때문이다.
참고
https://yozm.wishket.com/magazine/detail/2083
https://ui.dev/imperative-vs-declarative-programming
https://tech.kakaopay.com/post/react-query-2
Subscribe to my newsletter
Read articles from Jieun Lee directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by