[❗️Error] 선언형 프로그래밍을 해야 하는 이유

devAnderson·2023년 8월 5일
0

Error Handling

목록 보기
11/12
post-thumbnail

⭐ 기술 스택도 중요하지만

최근 새로운 기술들이 많이 쏟아져 나오면서, 신 문물(?) 들을 접해보며 이것저것 시도해보는 도전을 많이 해보고 있다.

예전에 Redux-thunk, Redux-saga를 공부하면서 골머릴 싸다가 redux-toolkit으로 넘어간 것처럼,
전역 상태관리를 redux로 하다가 요 근래에는 패러다임이 바뀌어 구독형 atom 단위 상태관리 툴인 recoil, jotai의 도입을 고민해보기도 하고 프론트 데이터에서 핵심적인 비동기 데이터 상태 그 자체에 관심사를 두는 react-query를 쓰게 되는것처럼 말이다.

그런 기술들을 공부하면서 진보하는 느낌도 들고 최신 동향을 따라가는 것은 의미가 있었지만, 정작 기초에 대해서 돌이켜보게 되는 시간을 갖지는 않았던 것 같다.

그리고 선언형 프로그래밍이 그랬다. 아주 예전에 공부했던 내용이고, 그 개념을 공부했기 때문에 "그래 난 이 개념을 알고 있어" 라고 착각했던 그것.

그런데, 알고 있는것과 정작 현실에서 사용하게 되는 것은 다르다는 것을 이번에 에러 핸들링을 하다가 절실하게 느끼게 되었다.

분명 더더욱 깊은 의미로 선배들이 이 패러다임을 만들게 된 이유가 있고, 더 깊게 파고들어가서 "아하" 하게 되는 순간을 마주해야하겠지만, 지금은 선언형 프로그래밍이라는 패러다임이 왜 생겨나게 된 것인지에 대한 내 나름대로의 이해를 이번 실수 대환장 파티에서 찾아보려고 한다.

기초는, 기초이기 때문에 쉽다는 것이 아니라 그만큼 가장 핵심적인 내용을 담고 있다는 뜻이다.
-by 앤더손씨-


⭐ 선언형 프로그래밍이란

사실 난 선언형 vs 명령형에 대해서 글만 읽어서는 잘 이해를 못했었다.

더 정확하게 말하면 코드를 보고 이해는 했는데, 왜 이런 내용을 굳이 차이점까지 두면서 설명하는것일까... 하는 의문에 빠져있다가 결국 지식적인 의미로서 머릿속에 담아두고 끝났었다.

A. 명령형 프로그래밍

일반적으로 명령형 프로그래밍이란, 컴퓨터가 수행할 명령에 대해서 어떤 동작을 해야 하는지 순서대로 표현하는 기법을 뜻한다. 해당 명령들을 통해 외부 상태를 어떻게 변경시키는지에 대해 관심을 둔다.

사실, 더 정확하게 말하자면 함수가 없던 시절 "순차적 프로그래밍" 당시가 모든 로직을 하나의 순서로 기법하여 따르게 하다가 필요에 따라 goto라는 형태로 실행 순서를 강제적으로 변경하는 방식으로 개발을 했었다.
그런데 이러한 방식은 흐름 제어를 어렵게 만들고 앱이 커질수록 복잡도가 늘어나갔다. 이런 상황을 타개하기 위해 반복되는 로직에 대해서 "함수"라는 개념으로 묶고 이것을 재사용하여 해당 호출이 끝나고 나면 다시 실행 흐름을 그 시점으로 돌아가 진행하게 만드는 절차적, 명령형 프로그래미잉 만들어지게 된다.

이 방식은 순차적 프로그래밍에 비해서 코드의 재활용성이라는 부분에서는 좋은 이점을 지녔지만, 문제는 일반적으로 절차적 프로그래밍이 전역 변수를 사용하다보니 규모가 커지면 커질수록 변수의 중복이 발생하는 상황이 너무 많이 벌어진다는 점이었다. 이로 인해 객체 지향적인 개발이 발전하게 된다.

여튼 좀 돌아갔는데, 결론적으로 말하자면 때 명령형 프로그래밍의 관심대상은 "컴퓨터의 동작들" 이다. 간단하게 예를 들자면

// 특정 고객의 어카운트 아이디를 가지고 체크아웃을 한다.
  let state = [....데이터베이스];
  
  if(typeof account === "string"){
  	account = Number(account)
  }

  let result;
  
  for(){
    .
    .
    .
  	데이터베이스에서 원하는 어카운트를 찾는 로직
    result = 조회결과;
  }

   if(result){
  	 for(){
       .
       .
       .
      	result에서 필요한 값만 찾는 로직 
     }
   }
}

// 결과적으로 위의 문들을 파싱하여 컴퓨터에게 넘겨주고 실행시킬 때 "체크아웃"이라는 목표를 달성.

위와 같이, 어떤 달성목표를 위해서 코드를 나열해나가는 방식이다. (집중을 동작 하나하나에 두고 있다고 보면 좋다)

당연하게도, 컴퓨터 세계에서 명령형은 너무나 당연한 것이다. 문이 없는데 어떻게 컴퓨터가 작동을 하겠는가.

어디까지나, 명령형 프로그래밍이라는 것은 개발자가 코드를 짤 때 동작 자체에 중점을 두고 작성한다는 것이라고 생각한다.

다만, 코드를 짜는 것은 사람이지 컴퓨터가 아니기 때문에

당연하게도 "컴퓨터 동작"을 집중하는 방식으로 코드를 나열하게 되면 후에 유지보수가 어렵게 된다.

위처럼 모든 동작들이 한 스크립트 안에 계속 나열되는 코드가 길어진다고 생각하면

에러 핸들링을 위해서 모든 코드를 다 파악해야 하는 상황이 벌어지기도 한다.

B. 선언형 프로그래밍

이런 문제로 인해, 새로운 패러다임으로 선언형 프로그래밍이라는 것이 나오게 되었다.

여러 정의가 있겠지만, 내가 이해하는 선언형 프로그래밍의 정의는 "개발자의 정신 모델" 에 부합하는 방식으로 코드를 짜는 것이다.

즉, 어떤 동작들의 집합에 대해서 "이건 이 목표를 위한 것이다!" 라고 개발자가 인간적으로 이해하기 쉽게 "추상화된 선언"을 내리는 방식이다.

const account = getAccount(id);

위의 내용을 보면, account라는 상수에 할당되는 값은 getAccount라는 함수의 매개변수에 id라는 인자를 넣어 호출한 결과라는 것을 알 수 있다.

위 내용에서 getAccount라는 함수의 내부 로직은 관심사에 없다.

내가 알고 싶은 것은 결국 account가 무엇인지일 뿐이다.

중요한 것은 결국 getAccount 안에 있는 동작들은 명령형으로 이루어져 있다는 점이다. 다만, 외부적으로 노출될 관심사는 오로지 getAccount의 결과일 뿐이다.

이처럼 명령형으로 이루어져있는 내용을 추상적인 "선언" 을 통해서 모아두는 선언형 방식은 함수형 프로그래밍과 결합하여 조금 더 개발자가 이해하기 쉽고, 가독성이 높으며, 추후 확장성과 함께 에러 핸들링을 하기 쉽게 만든다.


⭐ 실제 내가 했던 실수에서 살펴보자

사실 위에 내용까지는 그냥 내가 개념적으로 이해하고 있던 내용이었다. 그런데 이번에 개발을 하면서 여러가지 마주하던 에러들을 천천히 살펴보니 느꼈던 것은

어떤 코드들에 있어서 그 내용이 "순서적" 측면이 중요하게 여겨진다면, 선언형을 고려하는는 것이 정신건강에 이롭다.

라는 것이었다.

물론 가장 좋은 것은 코드에 있어서 순서가 필요하지 않은 로직을 짤 수 있는게 베스트이겠지만, 어쩔 수 없이 코드적인 순서가 필요하게 되는 경우도 있을 것이다.

아래는 내가 겪은 예다.

<에러 1>

위 내용은 컬러 스펙트럼 내에서 어느 지점을 클릭하고 있는징에 대해 캔버스로 클릭 지점을 생성하는 함수를 만들어둔 것이다.

함수 내용을 보면 context에 strokeStyle을 설정하려다가 에러가 난 것을 볼 수 있다.

context는

  const pickerCanvasCtx = useRef<CanvasRenderingContext2D | null>(null);

이런 useRef로 이루어져있는데, canvas 내부를 컨트롤할 수 있는 context를 매번 getContext()로 호출하며 가져오기보다 처음 호출한 인스턴스를 저장해두는 것으로 최적하기 위함이었다.

참고로 저 에러 부분에 "context?.strokeStyle = 'black'" 형태로 옵셔널 체이닝을 걸어줘도 되겠지만, 그것보다 로직적인 측면을 더 보려고 했다.

위 부분이 캔버스가 마운트 되고 난 이후 비동기 작업이 이루어지는 useEffect의 내부 내용이다.

저 picker pointer 함수를 따로 분리해두고 useEffect에 아무 생각없이 집어넣을 때에 아래 ref 할당부분을 전혀 고려하질 않았다.

어찌보면 너무 당연하고 바보같은 실수이긴 한데, 그 순간에는 너무 급했던지라 생각조차 하질 못했다. (부끄)

그런데 이런 실수가 빈번하게 일어나는 이유가 뭘지를 고민해보면 결과적으로는

논리적인 입장에서, 먼저 init과 관련된 로직이 다 일어난 이후 picker pointer를 생성해야 한다.

라는 "순서적 측면"이 제대로 고려되지 않았던 것이다.

즉, 개발할 때에 무언가 로직적으로 순서가 중요해보인다면 본능적으로 "이거 로직들을 선언해둬야 문제가 안될거같은데" 라고 여겨봄직 하다는 뜻이다.

그래서 위에처럼 아예 init과 관련된 로직들을 선언으로 분리한 후

위처럼 로직을 변경하였더니 에러는 당연하게도 없어졌다.
거기에 더불어서 가독성도 좋아졌다. 그 누가 보더라도 useEffect 안의 내용은 "무언가 init을 하고 picker pointer을 만들었구나" 하고 짐작하기 쉬워지는 것이다.

주의할 점은, 이런 선언적인 분리는 어디까지나 주관이 강하기 때문에 어디까지의 범위를 선언해주는 것이 올바른 것일지에 대해서는 많은 훈련과 고민이 뒤따른다는 점이다.

사실 init 내에 있는 initPicker, initColorBar 역시 전부 다 나름대로 선언을 하겠답시고 명령형 로직들을 모아둔 것이지만, 어디까지나 그냥 너무 난잡하게 퍼져 있는 명령형 코드들을 정리하고자 하는 목적이 강했는데

정작 선언형이 필요한 부분은 위처럼 init 그 자체와 picker pionter 생성에 대한 "순서적 측면" 을 고려해야 하는 부분이었다.

선언형 프로그래밍의 패러다임을 하면 더 좋은 것은 에러를 핸들링 할 때, 내가 어떤 포인트에 집중해서 에러를 봐야 하는지를 확실하게 이해하기 쉬워진다는 점이다.

아래는 내가 방금 전에 했던 바보같은 실수 2이다.

<에러 2>

위 로직은 반응형에 의해서 저장되는 기존 스냅샷 메모 데이터들을 indexedDB에 저장하기 위해 for문을 돌리는 부분이다.

그런데 정작 로직을 작성한 후 반응형 전의 메모만 반영되고 가장 최근에 작성한 메모는 업데이트가 안되는 문제가 발생해서 도대체 어디가 문제인지 확인이 안되어 스트레스를 많이 받았다.

그런데, 이 역시도 똑같이 순서적인 측면을 고려하지 않고 아무생각없이 제어문을 통한 명령형 로직을 작성한 결과였다.

사실은, 저 dataUrlList 배열 내부에 canvas.toDataURL() 부분은 현재 클라이언트 화면의 가장 최신 이미지이기때문에, 메모라이징된 반응형 전 스냅샷보다 더 이후에 들어가야 한다.

그런데 너무 생각없이 배열에다가 스냅샷 데이터를 넣어둔다는 배열을 먼저 명령적으로 적어놓고나니, 물 흐르듯이 다음 명령문을 작성해서 메모라이징된 이미지 데이터들을 삽입해야지.... 하는 방식으로 개발을 했던 것이다.

"선언형 프로그래밍은 개발자의 정신 모델에 집중한 패러다임이다" 라는 정의에 걸맞게, 내가 우선적으로 선언형에 대한 마인드셋을 장착했다면 아래와 같이 작성했던 것이 맞았던것 같다.

위의 이미지에서 보면 Array 에서 제공하는 메서드 "map" 함수 역시 선언적으로 함수를 돌면서 특정 동작을 한 뒤에 새로운 배열을 리턴하는 로직이 담긴 순수 함수이다.

이렇게 다른 선언적 순수함수를 이용해서 내가 for 제어문에서 하려고 했던 내용을 추상화하여 convertedMemorizedData 상수에 할당했다.

이렇게 환경을 설정하고 나니 순서가 명확하게 보이는 것이다.

아, 먼저 메모라이징 되었던 애들을 변환시킨 뒤에 그 데이터들을 배열에 집어넣어야지

하고 생각이 넘어가지는 것이다.

또한, 선언적으로 로직을 묶어 작성하고 나면 내가 어떤 부분을 집중해서 에러 핸들링해야 하는지 쉽게 로직 단위별로 테스트하기 쉬워진다.


위의 내용은 또다른 최적화를 위해 dataUrlList 로직 자체를 한번 더 선언적으로 정리한 부분이다.

이번에는 기존 에러와 다르게 반응형 전 스냅샷 이미지들이 웹에 반영되지 않길래 무슨 문제인가 싶었다.

하지만 선언적으로 정리를 해둔 것이 있었기 때문에 바로 원인 파악을 할 때 무엇을 집중할 지 알게 되었다.

"음.. 기존 이미지가 없네? 기존이미지는 어떻게 넣었더라? 아 내가 선언해뒀던 genDataList 함수에서 관리했었지. 그럼 거기안에 콘솔을 찍어보면 되겠구나.

하고 말이다.

만약 내가 이것을 또 명령적으로 나열해서 적어뒀다면 우선 그 코드들을 쭉 살펴보며 다녀야 하는 수고로움이 벌어졌겠지만, 저렇게 선언적으로 작성해두니 에러핸들링할 때 어떤 포인트를 집중해야 하는지 머릿속에 바로 떠오르니 효율적으로 에러가 관리될 수 있었다.


⭐ 마치며

선언적 프로그래밍이나 절차적 프로그래밍과 같은 개념들은 그 자체적으로 무언가 색다르고 신기한 기술은 아니다.

어찌 보면 개발 습관에 가까운 지식이라고도 할 수 있겠다.

하지만, 개발을 하는 주체는 결국 사람이고

사람이 하는 이상 휴먼 에러를 만날 수밖에 없다. 이런 휴먼 에러를 조금 더 효과적으로 컨트롤하고 무엇보다도 우선적으로 그런 휴먼 에러자체를 예방하는 방식으로 코드를 짜 나가기 위해서 이렇게 선배님들이 미리 경험하고 정립한 패더라임들을 익혀나가다보면

최종적으로 하나의 서비스에 있어서 더욱 가독성이 높고 최적화된 로직을 구현할 수 있을 것이라는 아하 포인트를 얻는 순간이었다.

profile
자라나라 프론트엔드 개발새싹!

1개의 댓글

comment-user-thumbnail
2023년 8월 5일

글 잘 봤습니다.

답글 달기