의식의 흐름으로 "선언형", "선언형 UI" 개념 재정리하기

ricky_0_k·2021년 12월 28일
2
post-thumbnail

난 예전에 선언적 UI 패턴을 이렇게 이야기한 적이 있었다.

선언적 UI 패턴은 개발자가 화면 구성을 확인하는 데 직관성을 제공하는 것은 물론이고,
오로지 View 의 상태에 대해서만 프로그래밍 할 수 있도록 돕습니다.

2년전에 이야기한 내용이었고, 지금도 처음 보는 사람에게 이렇게 설명이 가능할지 고민이 생겼다.

그리고 고민한 결과 아래의 아쉬운 점이 느껴졌다.

  1. 직관적이라는 게 what 을 이야기하는 것 같은데 확 와닿지 않는 느낌이었다.

  2. 일부 잘못 해석될 수 있는 내용도 있었다. 오로지 View 상태에 대해서만 프로그래밍 할 수 있다는 걸 보니,
    당시의 나는 한 곳에서 해당 View 에 대해 프로그래밍할 수 있다는 걸 이야기하고 싶었던 것 같다.

    그런 차원에서 보면 오로지 한 포인트에서 View 의 상태에 대해 프로그래밍 할 수 있도록 이 더 어울리는 말이었다.

그 외에도 나사가 부분부분 빠져 보였어서 다시 정리가 필요해보였다.

선언형, 선언형 UI 개념을 내 기억에 정리하면서
임펙트 있고 길게 기억하기 위해 이 포스트를 작성하게 되었다.

선언형?

선언형을 처음 듣는 사람에게 이걸 설명하려면 어떻게 해야할지부터 고민해보았다.

1. 위키 보기

일단은 진리의 위키백과(?)를 참고했고 아래의 문구를 보았다.

한 정의에 따르면, 프로그램이 어떤 방법으로 해야 하는지를 나타내기보다 무엇과 같은지를 설명하는 경우에 "선언형"이라고 한다. 예를 들어, 웹 페이지는 선언형인데 웹페이지는 제목, 글꼴, 본문, 그림과 같이 "무엇"이 나타나야하는지를 묘사하는 것이지 "어떤 방법으로" 컴퓨터 화면에 페이지를 나타내야 하는지를 묘사하는 것이 아니기 때문이다. 이것은 전통적인 포트란과 C, 자바와 같은 명령형 프로그래밍 언어와는 다른 접근방식인데, 명령형 프로그래밍 언어는 프로그래머가 실행될 알고리즘을 명시해주어야 하는 것이다. 간단히 말하여, 명령형 프로그램은 알고리즘을 명시하고 목표는 명시하지 않는 데 반해 선언형 프로그램은 목표를 명시하고 알고리즘을 명시하지 않는 것이다.

별로 글자도 안 봤는데 벌써부터 머리가 아파 핵심으로 보이는 내용을 블록 처리해 정리해보았다.

  1. "무엇"이 나타나야하는지를 묘사

    이 내용은 제법 봤었다.
    어떻게(how) 대신 무엇을(what)을 이야기하는 게 선언형 이라는 글을 많이 보았었다.

  2. 목표를 명시하고 알고리즘을 명시하지 않는 것

    what의 연장선의 내용이라 생각이 들었다.
    알고리즘이라는 것이 과정이라 생각하기 때문에 맞는 말이라 생각이 들었다.

2. 사례 보기

어떤 차이가 있는지는 얼추 이해했고 사례도 보고 싶었다.

1. Flutter 공식 내용 사례

이런 갈증에 맞춰 Flutter 공식 문서 내용에 사례가 있었고 제법 도움이 되었다.
사례 내용은 아래와 같다.

난 개인적으로 이 사례가 공감이 많이 되었다.
실제 타 블로그에서도 이 사례를 언급하기도 한다.

실제 동작의 차원에서 위 코드를 바라보며, 블록친 1,2번을 보충설명하면 아래와 같이 정리가 가능하다.

  1. "무엇"이 나타나야하는지를 묘사
    위의 코드(명령형)은 b가 어떻게 만들어지는지를 이야기하지만
    아래의 코드(선언형)은 ViewB 가 무엇 인지를 설명한다.

  2. 목표를 명시하고 알고리즘을 명시하지 않는 것
    위의 방식(명령형)은 b가 만들어지는 일련의 과정이 명시되어 있다.
    아래의 방식(선언형)은 ViewB 를 설명하는 목표에만 치중하고 있다.

2. 집 주소 설명 사례

그 외에 집주소를 설명하는 방식을 다룬 사례도 있었는데 그 사례도 제법 괜찮았다.
이 사례를 좀 내 마음대로 만들어보았다.

  1. 명령형 방식
    사거리에서 길 건너 300m 직진 후 좌회전하고,
    150m 쯤 가서 우회전하고,
    10m 쯤 직진해 수입맥주 4캔을 사고(..?),
    50m 더 가서 빨간집 대문 앞으로 가면 우리집에 도착합니다.
  2. 선언형 방식
    xx시 xx구 xx길 xxxx 입니다.

두 가지 방식 모두 우리 집에 대해 이야기하지만, 이야기하는 방식과 목표는 다르다.

3. 정리

공식문서들과 타 블로그들을 보고나서 난 선언형에 대해 이렇게 다시 정리했다.

선언형은 무엇 에 대한 이야기를 주로 다루며,
과정 나열보다는 이게 무엇인지를 한 줄에 설명하려 노력한다.

선언형 UI 패턴

자 이제 본론까지 왔다. 선언형 UI 에 대해 설명해보세요 라고 하면 난 어떻게 말할 수 있을까?

"요즘 개발자들이 즐겨보는 UI 패턴이라고 하며, 크게는 열광도 한다고 한다."

만약 내가 위와 같이 결론과 상황만 이야기하면, 지식수준에 대해 재평가 또는 매장(?)을 당할수도 있을 거라 생각된다.

난 아직 묻히고 싶지 않고, 재평가도 당하고 싶지 않기 때문에
"이게 무엇무엇이고, 타 패턴에 비해 두드러지는 장점과 단점은 무엇이다."
를 이야기할 수 있을 정도로만 다뤄보려 한다.

무엇?

다시 공부할겸 일부 블로그들을 검색해보았다.

  1. 첫 번째 정의에 입각해보기
    나는 앞서 선언형이 무엇인지를 분석했었다.
    이를 상기하여 나는 아래와 같이 말을 만들어보았다.

    과정보다는 무엇 에 대한 이야기를 다루는 UI 패턴

    평가 : 별로..
    적고나서 보니 너무 이론적이다. 내가 보기엔 별로인 것 같다.
    "치킨은 닭으로 만들어지고, 닭으로는 치킨을 만든다." 같이 의미없는 말의 집합체 같아 보인다.
    선언형의 다른 특징도 확인해보아야 겠다.

  2. 두 번째 정의에 입각해보기
    두 번째 정의를 활용해 말을 다시 만들어보았다.

    과정 나열보다는 이게 무슨 UI 인지를 한 줄에 설명하려 노력하는 패턴

    평가 : SoSo
    그나마 좀 나아보인다. 실제 Flutter 공식 문서 사례와도 일부 매치되기도 한다.
    말을 좀더 다듬어 봐야겠다.

  3. 다듬어 보기
    비교는 의미 없으니 빼고, 설명하려 노력하는 도 최대한 줄여 써봐야겠다.

    이게 무슨 UI 인지 한 번에 알 수 있는 패턴이다.

    평가 : 정의는 나름 된 듯
    한 문장으로 표현은 된 것 같다.

  4. 만드는 방식도 덧붙이고 멋드러지게 만들어보기
    한 문장으로 표현을 최대한 했지만 뭔가 아쉽긴 하다.
    만드는 방식도 좀 언급하고, 멋드러지게 표현하면 좋을 것 같다는 생각이 들었다.

    F(State) = View.

    검색해보니 만드는 방식을 공식으로 표현한 게 있었다.
    공식에 대한 설명도 추가해야겠다고 느꼈다.

    F(State) = View. 즉 상태를 가지는 함수를 호출하는 방식으로 UI 를 만드는 형태이다.

    평가 : 나름 Ok
    확실히 어떤 방식으로 만드는지도 설명되니 좋은 것 같다.
    자 그러면 위에서 언급했던 사례도 덧붙여 최종 정리를 해봐야겠다.

  5. 최종 정리

    이게 무슨 UI 인지 한 번에 알 수 있는 패턴이다.
    F(State) = View. 즉 상태를 가지는 함수를 호출하는 방식으로 UI 를 만드는 형태이다.
    예를 들면 명령형 형태로 주소는 .......

    평가 : 매장은 면할 수 있겠다.
    이 정도면 무엇인지와 더불어, 어떤 방식으로 UI 를 만드는지도 표현이 가능해보인다.
    사례도 더해졌으니 나름 공부는 했다고 평가받지 않을까 싶다.

장점

선언형 UI 패턴을 통해 얻는 장점은 뭘까?
난 먼저 정의를 다시 한 번 돌아보았다.

1. 이게 무슨 UI 인지 한 번에 알 수 있는 패턴이다. (직관적)

정의대로 이게 무슨 UI 인지 직관적으로 알 수 있는 패턴이다.
실제 개발 사례를 생각해보아야겠다.

  1. 안드로이드 네이티브에서 가장 일반적인 UI 를 만드는 과정

    일단 제일 크리티컬 한 건 관리포인트가 여러개 인거다.

    물론 Kotlin 으로만 UI 를 작성 할 수도 있겠지만 그건 입코딩이다.
    보통은 xml 과 Kotlin 을 같이 작성하는 게 편하다.

    그 이외에도 문자열이나 색상은 strings.xml, colors.xml 에 설정해야하고,
    경우에 따라 theme.xml 이나 styles.xml 에 추가 작업을 해야하고 등등.....

    고려할 게 많다.

  2. 안드로이드 네이티브의 선언형 UI 사례 (Jetpack Compose, Flutter 등)

    xml, Kotlin 으로 화면 코드가 분리하는 건 선언형 개념에 위배된 내용일 것이다.
    선언형 UI 는 무슨 UI 인지를 한번에 알아야한다 고 했다.
    그런데 화면에 대한 내용이 분리가 되어있다고 하면 원론적인 차원에서 모순일테니 말이다.

    그럼 선언형 UI 의 개념을 따르는 사례가 있을까?
    위에서 언급한 Jetpack Compose, Flutter 이다.

    Jetpack Compose 는 Kotlin 으로만 작성하고, Flutter 는 Dart 로만 UI를 작성한다.
    물론 여기에서도 문자열 등 리소스는 따로 관리해야 하지만,
    애니메이션이나 추가 설정은 코드 차원에서 처리가 가능하다.

    한 곳에서 UI 에 대한 정보를 다 알 수 있으니 첫 번째 장점 설명은 이걸로 끝!

정리

하나의 포인트에서 화면 구성, 이벤트 내용, 애니메이션 등 UI 의 모든 것을 알 수 있다.
이로 인해 UI 수정이 필요할 때, 해당 UI 에 직관적인 접근이 가능해진다.

2. 재사용성 (효율성)

왜 효율적인지를 파악하기 위해, 위의 일반 UI 사례선언형 UI 사례를 다시 가져와 보았다.

  1. 안드로이드 네이티브에서 가장 일반적인 UI 사례

    xml 과 Kotlin 을 같이 사용하는 경우,
    xml 은 보통 화면단을, Kotlin 은 일부 로직 쪽을 담당한다.

    여기서 재사용성 관련하여 질문을 하나 던져본다.

    1. 오로지 화면(xml) 만 따로 재사용할 수 있을까?
    2. 아니면 일부 로직(Kotlin) 만 따로 재사용할 수 있을까?
    3. 아니면 그 둘(xml, Kotlin)을 모두 재사용할 수 있을까?

    3개 모두 가능하다.
    둘을 모두 재사용하는 경우는, Dialog 나 CustomView 등의 사례로 보면 알 수 있다.
    다만 이 경우에는 xml, Kotlin 영역을 동시에 고려하면서 작업이 필요할 것 같다.

  2. 안드로이드 네이티브의 선언형 UI 사례
    1. Jetpack Compose
    Jetpack Compose 는 그냥 UI 를 호출만 해주면 된다.
    예를 들면 아래와 같이 말이다.

    override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       
       setContent {
          GraduationView(viewModel)  // UI 호출을 통한 생성
       }
    }

    GraduationView 하나만 만들고 매개변수를 고려해서
    어느 곳에서든 호출하게 만들면 쉽게 재사용이 가능하다.

    Kotlin, xml 의 경우 재사용 가능하도록 만들어주는 인위적 작업이 필요했다면
    Jetpack Compose 에서는 기본적으로 제공해주는 기능이다.

    2. Flutter
    Flutter 는 함수형태로 반환도 가능하다.
    이런식으로 만들고 이전 방식과 선언형 방식을 혼용할수도 있다.
    아래는 선언형 방식만을 사용해본 예이다.

    class LoadingWidget extends StatelessWidget {
       
       Widget build(BuildContext context) {
          return const Center(
             child: CircularProgressIndicator(),
          );
       }
    }

이렇게 2가지 사례를 보았다.

물론 개발자의 역량에 따라 달라질 순 있다.
하지만 선언형 UI 를 사용하면 재사용성 차원에서 아래 이득을 볼 수 있을 것 같다.

  1. 인위적 작업 없이 기본 호출만으로 재사용을 할 수 있다
  2. 하나의 영역만 바라보고 관리가 가능하므로 (2 -> 1)
    유지보수 작업 효율성에도 조금 더 나은 느낌이다.

정리

선언형 UI 패턴은 개발자의 인위적 작업 없이 기본 호출만으로 UI 를 재사용할 수 있다.
UI 를 수정하려면 하나의 영역만 확인 후 수정하면 되니, 작업 효율성도 올라가는 느낌이다.

3. 단방향 흐름으로 인한 SideEffect 제거

이건 Jetpack Compose 에서 더욱 체감할 수 있는 요소이다.
JetPack Compose 에서 UI 는 Composable 어노테이션을 가지는 함수 를 통해 만들 수 있다.

@Composable
fun GraduateView(){
	// ...
    Column() {
    	GraduateTextView(),
        GraduateTextView()
    }
}

@Composable
fun GraduateTextView(){
	// ...
}

위와 같이 Composable 함수를 선언하고 조합하여 커스텀 UI 를 만들 수 있다.
(후술하겠지만 Column 도 Composable 함수이다)

여기서 주목할 내용은 Composable 함수들은 전부 반환값이 없다.
이건 단방향의 중요한 단추이다. 실제 아래 예시를 보자.

@Composable
inline fun Column(
    modifier: Modifier = Modifier,
    verticalArrangement: Arrangement.Vertical = Arrangement.Top,
    horizontalAlignment: Alignment.Horizontal = Alignment.Start,
    content: @Composable ColumnScope.() -> Unit
) {
    val measurePolicy = columnMeasurePolicy(verticalArrangement, horizontalAlignment)
    Layout(
        content = { ColumnScopeInstance.content() },
        measurePolicy = measurePolicy,
        modifier = modifier
    )
}

이 코드는 위에서 썼던 Column 의 정의 내용이다.
안드로이드 UI 에서 orientation 을 vertial 로 갖는 LinearLayout 을 생각하면 된다.
(= 수직 정렬 레이아웃)
실제 이 코드는 보다시피 반환값이 없다. 다른 내장 UI 함수(Text, TextField 등)도 마찬가지이다.

반환값이 없다는 건 아래 내용을 의미한다.

  1. UI 를 호출하는 영역(ex. Activity)에서, UI 내의 로직(ex. Composable 함수 내부)에 직접 접근할 수 없다.
    ex. Activity 위치에서 호출한 Compose UI 내의 로직에 접근할 수 없다.

  2. UI 내(ex. Composable 함수 내부)에서, UI 를 호출하는 영역(ex. Activity) 의 로직을 직접 실행할 수 없다.

    물론 인위적으로 매개변수를 주입하여 접근할수록 만들 순 있다

    ex. Compose UI 에서 호출한 Activity 의 로직에 접근할 수 없다.
    단 매개변수를 통해 간접적인 접근은 가능할 수는 있다
    (ex. Activity 또는 ViewModel 을 매개변수로 주입)

정리하면 호출한 영역 -> 호출된 영역으로 향하는 단방향 흐름으로 설명될 수 있다.

단방향 흐름의 장점

만약 양방향 흐름으로 서로와 맞물려 있는 로직을 수정할 경우, 각 파일들을 왔다갔다하면서 수정해야한다.
깊게 들어가면 클린 아키텍처에서 나쁜 사례로 언급했던 순환의 지옥을 맛볼 수 있다.

단방향에서는 서로 맞물리는 케이스가 비교적 적다.

이 덕분에 단방향 흐름에서는 특정 UI 를 수정해야 할 시, 해당 UI 코드만 신경쓰고 수정하면 된다.

만약 호출부에서 넘겨주는 매개변수로 서로 맞물리는 케이스가 있을 경우에는
넘겨주는 값과 로직이 제대로 UI 코드에 반영되는지만 신경쓰면 된다.

참고 : 위에서 말했듯이 Jetpack Compose 는 특히 단방향적 성향이 강해 (반환값이 없음)
아키텍처 상에서 MVP 구조에서는 사용이 힘들고, MVVM 구조에서는 사용이 용이한 편이다.

정리

선언형 UI 패턴은 대체로 단방향 흐름의 성향이 강하다.
이럴 경우 호출된 영역과, 호출한 영역이 서로의 로직을 침범할 수 있는 방법이 없다.

이는 각 영역의 독립성을 높여주어, 서로를 참조하면서 발생되는 SideEffect 차단에 도움이 된다.

단점

특징에서부터 단점을 생각해보려 한다.
한 곳에서 UI 의 모든 것을 볼 수 있는 선언형 UI 에서 비롯되는 단점은 무엇일까?

1. 거대화된 단위 코드

신경써야 할 내용이 하나로 합쳐진다고, 신경쓰는 개수까지 줄어드는 건 아니다.
모든 내용이 한 파일에 있어 UI 하나가 거대한 코드 단위일 것이다.

한 곳에서 모든 걸 설명하지만 그만큼의 코드는 많아지기에
Trade Off 현상 (= 얻는 게 있으면 잃는 것도 생기는 현상) 으로 안고가야 할 내용이다.

정리

한 곳에서 UI 의 모든 것들이 정의되어 있기에 거대화 되어 있는 코드를 보게 된다.

2. 재사용성의 어두운 면

위에서 장점으로 재사용성(효율성)을 이야기했다.
하지만 이는 장점이면서도, 그만큼 개발자가 재사용성이 높게 만들어야 하는 것이다.

만약 개발자가 재사용 없이, 각 화면마다 일일히 UI 를 선언해준다면 어떨까?
동일한 기능의 코드가 산발적으로 퍼져 오히려 코드 라인이 획기적으로 늘어난 것을 확인할 수 있다.

그렇다고 무조건 중복 코드를 합치는 게 능사는 아니다.
한 곳에 두고 온갖 조건문을 두는 것보다 차라리 분리시키는 게 더 나을수도 있다.

이런 판단이 필요하기에 많은 경험을 통해
본인만의 재사용성 활용 스킬을 높이는 게 필요하다고 생각한다.

정리

재사용성은 개발자가 얼마나 활용하느냐에 따라 잘 쓰일 수 있다.
이에 대한 경험 없이 마구잡이로 사용하면 오히려 중복 코드가 많아져 유지보수가 힘들어질 수 있다.

선언형은 명령형의 추상화

선언형은 명령형 프로그래밍을 추상화했다고도 이야기한다.
사실이다. 실제 아래의 코드도 선언형 프로그래밍의 예 중 하나이다.

function declarative(arr) {
	return arr.reduce((acc,v)=>v+acc,0);
}

선언형 프로그래밍을 활용해 배열내의 값들을 모아 합을 구하는 함수로써
코틀린에서도 이런 선언형의 조합을 chaining 형태로 계속 이어 사용하기도 한다.

listOf(1,2,3,4,5).filter { it < 3 }.first()

위의 예도 선언형 프로그래밍이라 볼 수 있고
filter(), first() 가 어떻게 구현되어 있는지 확인하면
명령형으로 코드가 작성되어 있는 걸 확인할 수 있다.

과정 없이 단순 호출만을 하기 때문에 이런 프로그래밍 방식을 싫어하는 사람들도 있을꺼라 생각이 든다.
하지만 개인적으로는 나의 코드를 상대방이 직관적으로 보고 이해하는 게 더 중요하다고 생각하기 때문에
그런 차원에서 가독성에 큰 도움을 주는 선언형 프로그래밍에 긍정적이다.

결론

의식의 흐름대로 정리하면서 내 머리속에 선언형 프로그래밍과 선언형 UI 에 대해 다시 정리할 수 있었다.
다시 선언형을 공부하면서 앞으로 대세가 될 거라는 내용에 굉장히 공감했다.

하지만 단점에서 언급한 재사용성의 이면명령형을 추상화 때문에 개인적으로 우려되는 점도 있다.

어디까지 재활용 범위를 둘 것인지는 오롯이 개발자의 몫이기에
그 경험치에 따라 최고의 코드 또는 안티 패턴의 코드가 될 수 있다고 본다.
시행착오를 겪으면서 많이 접해보고 자신만의 방식을 찾아내야 할 것으로 생각한다.

그리고 명령형이 추상화 되어 있다고 그 로직을 알 필요가 없다는 말은 아니다.
추상화된 명령형 로직이 어떻게 동작하는지 확인해야 예상치 못한 성능 이슈를 해소할 수 있다.
편하다고 막 사용했다가 서비스에 치명상을 줄 수 있기 때문에
이 역시 시행착오를 겪으면서 해당 선언형 함수가 어떻게 동작하는지도 숙지할 필요가 있다고 생각한다.

선언형 UI 는 개발자의 노력에 따라 숙련도가 늘어나는 영역이라 생각이 들었다.

많이 사용하고 자신만의 깨달음을 얻는다면 요즘 개발자들이 즐겨보는 UI 패턴
능수능란하게 사용할 수 있지 않을까 생각하면서 이 포스트를 마무리한다.

profile
valuable 을 추구하려 노력하는 개발자

0개의 댓글