HomeBlog
Back to Blog

Contents

    Copyright 2025. Kwangmin. All rights reserved.
    2026년 1월 21일조회수:

    선언적 절차적 프로그래밍 그리고 추상화

    회사에서 선언적 프로그래밍에 대한 착각과 오해 좋은 포스트를 공유받아서, 포스트를 읽고 선언적 프로그래밍의 대해 정리하고 AI시대에서 선언적 프로그래밍은 어떤 value가 있을지에 생각이 담긴 글 입니다.


    선언적 프로그래밍의 본질, 명령의 나열에서 관계의 정의로

    리액트로 개발을 하다보면 자주 "선언적인 코드"이라는 단어를 마주합니다. 처음엔 그저 for 문 대신 map을 쓰는 것, 컴포넌트 레벨에서 UI와 데이터를 결합하는 코드가 HTML처럼 생긴 JSX를 쓰는 것이 선언적인 것이라고 막연하게 생각하곤 했습니다.

    하지만 실무에서 복잡한 비즈니스 로직을 다루다 보면 의문이 생깁니다. "결국 나도 if 문으로 분기 처리하고, 데이터를 순서대로 가공하고 있는데, 이게 정말 절차형 방식과 다른 게 뭘까?"

    오늘은 그 모호함을 걷어내고, 선언적 프로그래밍의 '시간의 흐름'을 '논리적 관계'로 치환하는 사고에 대해 이야기해보려 합니다.


    1. 선언적 사고와 명령형 사고의 간단한 예시

    저는 이 간단한 예시가 크게 도움이 되었습니다. 선언적 사고를 이해하는 가장 빠른 방법은 일상 속에서 "목표를 달성하는 방식"의 차이를 보는 것입니다.

    • 명령형(Imperative) 사고: '상세 레시피'
      • "물을 끓인다 → 면을 넣는다 → 3분 뒤 스프를 넣는다."
      • 이것은 시간의 순서가 핵심입니다. 한 단계가 어긋나면 전체 시스템이 무너지는 '절차'의 세계입니다.
    • 선언적(Declarative) 사고: '목적지 설정'
      • "이 요리는 익은 면과 매콤한 국물의 조합이다."
      • 이것은 구성 요소들 사이의 불변하는 관계를 정의합니다. 어떻게 조리될지는 시스템(엔진)에 맡기고, 우리는 '결과물의 정의'에 집중합니다.

    2. 함수형 도구는 죄가 없다: 중요한 건 '추상화의 의도'

    단순히 filter나 map을 쓴다고 선언적인 코드가 되는 것은 아닙니다. 그 도구를 다루는 개발자의 의도가 어디를 향하고 있느냐가 핵심입니다.

    2.1. 도구만 선언적일 뿐, 여전히 '절차'를 설명하는 코드

    // 읽는 사람은 변수 상태가 변하는 '과정'을 머릿속으로 시뮬레이션해야 함
    const getDiscountedProducts = (products) => {
      return products.map(p => {
        let finalPrice = p.price;
     
        if (p.category === 'sale') finalPrice *= 0.8;
     
        return { ...p, finalPrice };
     
      }).filter(p => p.stock > 0);
    };

    2.2 비즈니스 '관계'를 선언하는 코드

    const getDiscountedProducts = (products) => 
      products
        .map(applySaleDiscount)
        .filter(isInStock);
     
    // 각각의 함수가 '무엇'인지 이름표(추상화)를 붙여 관계를 선언함
    const applySaleDiscount = (p) => ({
      ...p,
      finalPrice: p.category === 'sale' ? p.price * 0.8 : p.price
    });
    const isInStock = (p) => p.stock > 0;

    두 번째 예시에서 우리는 더 이상 값이 어떻게 변하는지 추적하지 않습니다. 대신 할인을 적용하고, 재고가 있는 것만 남긴다 는 논리적 흐름(What)을 읽게 됩니다.


    3.JSX의 내부 추상화

    우리가 JSX가 선언적이다 라는건 단순히 HTML과 닮아서가 아닙니다. DOM을 직접 만지는 고통스러운 '명령'의 과정을 추상화해주기 때문입니다.

    왜 JSX가 선언적일까? 과거의 명령형 방식(Vanilla JS)은 "A 요소를 찾아서 B 텍스트를 넣고 C 부모에 붙여라"라는 시간적 순서에 의존했습니다. 이는 UI가 복잡해질수록 '동기화 오류'를 필연적으로 만듭니다.

    반면 JSX는 이 컴포넌트의 UI 구조는 이러하다라고 선언할 뿐입니다.

    가독성: UI의 계층 구조가 한눈에 들어옵니다.

    안정성: 상태(State)와 결과(UI) 사이의 연결 고리를 시스템이 보장합니다.

    이면의 추상화: 가상 DOM의 희생 리액트로 컴포넌트를 작성할때, 리액트는 내부적으로 추상화를 시켜놓았습니다.

    • 우리의 선언(JSX)을 자바스크립트 객체(Virtual DOM)로 변환합니다.

    • 이전 상태와 비교(Diffing)하여 어떤 절차적 명령(appendChild, removeChild 등)이 필요한지 계산합니다.

    • 최소한의 명령형 API를 호출하여 실제 화면을 업데이트합니다.

    리액트로 선언적인 코딩할 수 있는 이유는, 라이브러리가 가장 효율적인 명령형 절차를 내부적으로 추상화를 시켜놨기 때문입니다.

    3.1. 절차적 방식: "시간의 순서"에 의존하는 UI 생성

    우선 순수 자바스크립트로 UI를 만드는 명령형(Imperative) 코드를 보겠습니다.

    function createProfileCard(user) {
      // 1. 빈 박스를 만든다
      const card = document.createElement('div');
      card.className = 'card';
      
      // 2. 이름을 담을 제목을 만들어 박스에 넣는다
      const title = document.createElement('h1');
      title.innerText = user.name;
      card.appendChild(title);
      
      // 3. 조건이 맞으면 설명 요소를 만들어 박스에 넣는다
      if (user.bio) {
        const description = document.createElement('p');
        description.innerText = user.bio;
        card.appendChild(description);
      }
      
      return card;
    }

    이 방식의 핵심은 시간적 순서입니다. “먼저 이것을 만들고, 그다음 저것을 붙여라”라는 명령의 나열이죠. 만약 실수로 appendChild의 순서를 바꾸면 화면의 레이아웃이 완전히 깨져버립니다. 개발자는 코드를 읽으며 머릿속으로 DOM이 조립되는 과정을 시뮬레이션해야만 합니다.

    3.2. 선언적 방식: "구조적 관계"를 정의하는 JSX

    같은 기능을 리액트의 JSX로 표현하면 사고의 중심이 달라집니다.

    function ProfileCard({ user }) {
      return (
        <div className="card">
          <h1>{user.name}</h1>
          {user.bio && <p>{user.bio}</p>}
        </div>
      );
    }

    JSX는 "ProfileCard라는 존재는 타이틀과 설명이 포함된 계층적 구조다"라고 선언합니다. 여기서는 무엇을 먼저 생성할지 고민하지 않습니다. 요소들 사이의 포함 관계와 데이터와의 논리적 연결만 중요할 뿐이죠.

    3.3. JSX가 주는 이점

    단순히 가독성이 좋아지는 것 이상으로, JSX를 통한 선언적 프로그래밍은 개발자에게 고통을 덜어줍니다.

    • 동기화의 자동화: 명령형 방식에서는 데이터가 변할 때마다 "어떤 DOM 노드를 찾아가서 어떤 값을 바꿀지" 다시 명령해야 합니다. 하지만 JSX는 상태(State)가 변하면 그에 맞는 새로운 UI 구조를 리액트에게 던져줄 뿐입니다. "데이터와 UI를 일치시키는 고통스러운 작업"을 시스템이 대행해 주는 것이죠.

    • 이면의 추상화: 우리가 UI를 선언할 때, 리액트 밑바닥에서는 가상 DOM(Virtual DOM)을 활용한 지독하리만큼 정교한 절차적 엔진이 돌아갑니다. 이전 상태와 현재 선언을 비교(Diffing)하고, 실제 DOM에 적용할 가장 효율적인 명령형 절차(Reconciliation)를 생성합니다.

    결국 우리가 선언적으로 코딩할 수 있는 이유는 리액트가 복잡한 추상화 처리와 절차형 DOM 조작 절차를 우리 대신 수행해주고 있기 때문입니다


    4. 추상화는 상대적 useEffect vs Suspense

    선언적이라는 개념은 절대적인 구분이 아니라 "어떻게(How)를 어느 레벨까지 숨겼는가" 에 따른 상대적인 개념입니다. 우리가 흔히 쓰는 데이터 페칭 로직으로 이를 비교해 봅시다.

    4.1 useEffect, 상대적으로 명령형(절차적)인 접근

    대표적적인 데이터 페칭 필요한 useEffect훅을 사용하면 개발자는 데이터가 비어있는 '상태'가 아니라, 데이터를 가져오는 '동작의 순서'를 직접 제어해야 합니다. 또 어떤 문제가 있냐면 개발자가 로딩 처리, 데이터 업데이트, 에러 처리 등의 실행 순서(Step)를 일일이 관리해야 합니다. 흐름이 복잡해질수록 상태가 꼬일 확률이 높습니다.

    function UserProfile({ userId }) {
      const [user, setUser] = useState(null);
      const [isLoading, setIsLoading] = useState(true);
      const [error, setError] = useState(null);
     
      useEffect(() => {
        // 1. 로딩 상태를 켠다
        setIsLoading(true);
        // 2. 데이터를 요청한다
        fetchUser(userId).then((data) => {
          // 3. 데이터를 상태에 저장한다
          setUser(data);
          // 4. 로딩 상태를 끈다
          setIsLoading(false);
        }).catch((err) => {
          setError(err);
          setIsLoading(false);
        });
      }, [userId]);
     
      if(!user) return null;
      if(error) return <div>Error: {error.message}</div>;
      if (isLoading) return <LoadingSpinner />;
      return <div>{user.name}</div>;
    }

    4.2 Suspense,"이 데이터는 아직 준비 중일 수 있고, 준비되면 이 UI다"라는 상태 간의 관계를 선언합니다. (상대적 선언형)

    // 1. 컴포넌트는 "데이터가 이미 있다"고 가정하고 UI만 선언
    function UserProfile({ userId }) {
      const { data: user } = useSuspenseQuery(['user', userId], () => fetchUser(userId));
      return <div>{user.name}</div>;
    }
     
    // 2. 외부에서 로딩 상태와의 관계를 선언적으로 정의
    function App() {
      return (
        <ErrorBoundary fallback={<div>Error</div>}>
          <Suspense fallback={<LoadingSpinner />}>
            <UserProfile userId={123} />
          </Suspense>
        </ErrorBoundary>
      );
    }

    왜 선언적인가? UserProfile 내부에는 데이터를 가져오는 '순서'나 로딩을 끄고 켜는 '절차'가 없습니다. 단지 "이 컴포넌트는 해당 ID의 유저 데이터와 매칭된다"는 관계만 존재합니다. 로딩 시 UI를 갈아끼우는 지저분한 절차는 React와 Suspense가 내부적으로 알아서 수행합니다.


    5. 절차적은 언제 사용될까?

    그렇다면 절차적(명령형) 접근은 구시대의 산물일까요? 결코 그렇지 않습니다. 우리가 선언적 코드를 작성할 수 있는 이유는, 그 밑바닥에 단단하고 명확한 절차적 코드가 존재하기 때문입니다.

    5.1 최적화와 저수준 제어

    선언적 코드는 엔진(런타임)이 한 단계를 더 거쳐 해석해야 합니다. 하지만 1ms가 아쉬운 고성능 계산이나 수만 개의 데이터를 다루는 그래픽 렌더링에서는 엔진의 판단에 맡기기보다 개발자가 직접 메모리와 CPU의 동선을 짜주는 것이 유리합니다.

    // 수만 명의 유저 데이터 중 특정 조건을 만족하는 첫 번째 유저를 찾는 '최적화' 루프
    // 선언적인 filter().map() 보다 조기 종료(break)가 가능한 for문이 훨씬 빠를 수 있습니다.
    function findFirstValidUser(users) {
      for (let i = 0; i < users.length; i++) {
        if (isValid(users[i])) {
          return transform(users[i]); // 조건 충족 시 즉시 종료하는 절차적 최적화
        }
      }
    }

    5.2.부작용(Side Effects) 핸들링

    네트워크 통신, 타이머, 하드웨어 제어 같은 현실의 일들은 본질적으로 시간의 순서를 따릅니다. "토큰이 만료되었으면 -> 갱신 요청을 보내고 -> 성공하면 원래 요청을 재시도한다"는 로직을 선언적으로만 짜려고 하면 오히려 코드가 더 꼬이게 됩니다.

    5.3. 추상화 처리에 대하여

    우리가 흔히 사용하는 React Query, Redux, 심지어 React 자체의 소스코드를 직접 확인해보면. 그 안은 "큐를 비우고, 인덱스를 비교하고, 우선순위를 정해 스케줄링하는" 지독하리만큼 정교한 절차적 로직으로 가득 차 있습니다. 절차적 접근은 피해야 할 대상이 아닙니다. 오히려 선언적 추상화를 시키기 위해 우리가 반드시 마스터해야 할 가장 강력한 도구입니다. 선언적 프로그래밍은 명령형을 부정하는 개념이 아닙니다. 오히려 명령형을 깊이 이해한 사람만이 제대로 선언할 수 있을것 같습니다.


    6. AI 코딩 시대에 선언적 코드의 의미 (지극히 개인생각)

    "AI 코딩 시대에 굳이 선언적 코드가 의미가 있을까?" 라는 생각을 했던 것 같습니다.

    나중에는 AI가 0과 1의 바이너리 값만 가지고 코딩하는 게 오히려 컴퓨터가 이해하기엔 더 편하지 않을까? 결국 리액트나 리액트 쿼리 등 이런 도구들은 '사람'이라는 병목이 개발을 더 편하게 하려고 만든 '인간용 도구'니까요. 반대로 생각해봤을때는 오히려 이런 시대이기에 선언적 사고가 더 빛을 발하지 않을까? 라는 생각도 들었습니다.

    6.1. AI와의 '커뮤니케이션 효율'이 달라진다.

    AI(LLM)에게 코드를 시킬 때, 우리가 명령형으로 하나하나 지시하면 프롬프트가 엄청나게 길어지고 복잡해집니다. 반대로 우리가 선언적인 추상화 체계를 잘 잡아두면 AI에게 "이 비즈니스 관계를 코드로 구현해줘"라고 한 줄만 말해도 정확한 결과가 나옵니다. 즉, 추상화 레벨이 높은 코드일수록 AI가 이해하기 좋은 '고급 언어' 역할을 하게 되는 거죠.

    6.2. '코드 작성'은 AI가 하지만, '코드 리뷰'는 사람이 한다.

    이제 개발자의 역할은 'Writer'에서 'Reviewer'로 옮겨가고 있다고 생각합니다. AI가 뱉어낸 수백 줄의 코드가 절차적(명령형)으로 뒤엉켜 있으면 사람이 읽고 버그를 찾기가 거의 불가능합니다. 반면 선언적으로 잘 짜인 코드는 AI가 짠 로직이 내 의도(What)와 일치하는지 바로 확인할 수 있죠. 결국 유지보수의 주권은 여전히 사람에게 있고, 그 주권을 지키는것 중 하나가 선언적 코드입니다.

    6.3. AI는 '절차'는 잘 짜도, '구조'는 인간이 잡는다.

    AI는 특정 함수 내부의 로직(절차)을 구현하는 데는 사람보다 월등하지만, 전체 서비스의 도메인 모델과 컴포넌트 간의 논리적 관계(관계 선언)를 설계하는 건 여전히 인간의 영역이라고 생각합니다. 우리가 선언적으로 '지표'를 잘 설정해두면, AI는 그 지표를 달성하기 위한 '최적의 절차적 코드'를 짜주는 환상의 파트너가 될 수 있습니다.

    6.4. "추상화의 비용"이 0에 수렴

    과거에는 선언적 코드를 위해 추상화 계층을 만드는 게 시간 낭비처럼 느껴질 때도 있었죠. 하지만 이제는 AI가 복잡한 절차적 로직을 선언적 인터페이스로 순식간에 래핑(Wrapping)해줍니다. 우리는 "어떤 관계가 우리 서비스에 필요한가?"라는 본질적인 고민만 하고, 지저분한 구현은 AI에게 던져버리면 되는 시대가 앞으로 더 오지 않을까.

    AI 시대의 개발자는 단순히 task만 처리하는 코더가 아니라 전반적인 프로젝트 및 코드의 흐름을 파악하는 설계자가 되어야 한다고 생각합니다. "어떻게(How) 구현할까?"는 AI가 제일 잘하는 분야니 맡겨버리고, 우리는 "무엇(What)을 만들 것인가?"를 선언적으로 정의하는 능력을 키우는 게 훨씬 중요해진 거죠. 결국 선언적 사고는 AI라는 강력한 엔진을 조종하는 '핸들' 같은 존재가 되지 않을까?

    Reference

    • 선언적 프로그래밍에 대한 착각과 오해
    Tags
    react
    선언적프로그래밍
    추상화
    AI가 잘하는 시대일수록 펀더멘털은 더 중요해진다
    Generator로 상태흐름 제어하기

    댓글 로딩 중...

    조회수:0