abonglog

          • 소프트웨어 개발방법론

            • 로우파이 와이어프레임과 하이파이 와이어프레임
          • 자료구조 및 알고리즘

            • 다익스트라 알고리즘
            • 플로이드-워셜 알고리즘
            • 외판원 순회 문제(TSP) 를 완전 탐색 , DP로 풀어보자
            • 순열,조합과 그래프의 관계에 대해 알아보자
            • 백준 10986 - 나머지합 (모듈러 연산 , 누적합, 중복조합)
          • 함수형 자바스크립트

            • 모나드와 함께하는 함수형 프로그래밍 - Maybe 모나드
            • 복잡한 상태관리, 함수형으로 생각하며 리팩토링하기
            • 이터레이터와 이터러블, 제네레이터, 비동기 이터러블
            • 멀티패러다임 프로그래밍 서적 리뷰
            • 제네레이터를 이용해 자바스크립트의 큐 자료구조 10줄로 구현하기
            • 함수형 자바스크립트 모나드 알아보기
            • 함수형 자바스크립트의 펑터와 적용형 펑터
            • 커링 (currying) 에 대해 알아보자
            • 함수형 프로그래밍의 정의와 기초지식 및 가볍게 살펴보는 활용 예제
            • 함수형 자바스크립트 프로그래밍 학습 커리큘럼
          • 컴퓨터 공학 지식

            • 고급 프롬프트 엔지니어링을 위한 개념 정리
            • 개방형 와이파이에서도 폼 데이터는 안전할까 ?
          • 독서 노트

            • 솔로프리너의 시대 서평 리뷰
          • 웹 브라우저 지식

            • 경험에 의거한 FSD (Feature Sliced Design) 구조 완전 공략
            • zustand는 어떻게 마법같이 동작할까?
            • 이번에 합성 컴포넌트를 이용하여 디자인 시스템을 만들어봤던 경험
            • 함수형 컴포넌트의 useEffect에 대한 사견, 부수효과 관점에서 다시 보기
            • 브라우저의 캐시 사용법 및 NextJS 에서 캐시를 사용하는 방법
            • NextJS 는 어떻게 이미지 최적화를 구현하는가 ?
          • introduction to algorithms

            • 이진 검색 트리 (이진 탐색 트리)
          • mostly-adequate-guide

            • Chapter 13: Monoids bring it all together [번역]
            • Chapter 12: Traversing the Stone [번역]
            • Chapter 11: Transform Again, Naturally [번역]
            • Chapter 10: Applicative Functors [번역]
            • Chapter 09: Monadic Onions [번역]
            • Chapter 08: Tupperware [번역]
            • Chapter 07: Hindley-Milner and Me [번역]
            • Chapter 06: Example Application [번역]
            • Chapter 05: Coding by Composing [번역]
            • Chapter 04: Currying [번역]
            • Chapter 03: Pure Happiness with Pure Functions [번역]
            • Chapter 02: First Class Functions [번역]
            • Chapter 01: What Ever Are We Doing? [번역]
          • Zero to One

            • 2번의 프로젝트 관리 실패로 배운 1인 개발의 씁쓸한 회고록
            • Zero to one 시리즈를 시작하며
          abonglog logoabonglog
          복잡한 상태관리, 함수형으로 생각하며 리팩토링하기 의 썸네일

          복잡한 상태관리, 함수형으로 생각하며 리팩토링하기

          함수형 자바스크립트
          프로필 이미지
          yonghyeun7/10/2025, 2:17:26 AM

          최근 진행하고 있는 사이드 프로젝트에서 절차 지향적으로 작성된 코드들을 함수형으로 리팩토링 하며 느꼈던 점들을 기존 지식들을 회상하며 다시 작성한다.

          함수형 패러다임이란

          함수형 패러다임이란 모든 프로그램을 하나의 함수로 보고 그 안에서 다양한 순수 함수들을 합성하여 함수를 만들어내는 프로그래밍 패러다임을 의미한다.

          note

          순수함수란 동일한 입력에 대해 항상 동일한 출력값을 반환하는 함수를 의미한다. 순수 함수라는 점이 함수형 패러다임에서 중요한 이유 는 함수 합성 과정에서 부수효과가 존재하는 함수의 경우 매번 동일한 입출력을 보장 할 수 없어 안정성이 떨어지기 때문이다.

          함수형 패러다임에서 가장 큰 목적으로 두는 부분들은 다양하지만 그 중 이번 리팩토링 단계에서 느꼈던 점은 함수 합성과 지연평가이다.

          코드스멜이 났던 기존 구조

          우선 백그라운드를 좀 살펴보자

          현재 나는 블록 기반 텍스트 에디터를 만들며 블록 기반 에디터의 input 요소들을 관리하는 스토어를 생성하고 있다.

          인풋 요소들을 Segment라 정의했다.인풋 요소들을 Segment라 정의했다.

          현재 zustand 를 이용하고 있는 상태 관리 스토어에서 거대한 상태의 인터페이스는 이렇게 생겼다.

          실제 Store에서 사용하는 State, 중첩된 Map 형태
          interface Segment {
            /* ... 생략 */
            clientSegmentId : string;
            segmentText : string;
            segmentId : null | string;
          }
           
          export type ParagraphOrder = number;
          export type SegmentOrder = number;
           
          export type SegmentMap = Map<SegmentOrder, Segment>;
          // 📌 실제 Store에서 사용하는 State, 중첩된 Map 형태
          export type ParagraphMap = Map<ParagraphOrder, SegmentMap>;

          이런 중첩되어있는 Map 형태에서 가장 말단에 존재하는 특정 Segment 를 수정하기 위한 상태 변경 메소드는 changeSegment 메소드는 이런 타입으로 구현되어 있었다.

          변경하길 원하는 Segment의 식별자와 새로운 Segment를 인수로 받았음
            changeSegment: (segmentChange: {clientSegmentId : string; newSegment : Segment}) => void;

          이후 스토어 내부에서 인수로 받은 식별자인 clientSegmentId 를 이용해 중첩되어 있는 Map 내부에서 변경하고자 하는 Segment 를 찾아내고 두 번째 인수로 준 newSegment 로 교체하는 과정을 거친다.

          기존 구조의 코드 스멜을 알아보자

          함수에서 인수를 원시값이나 객체로 받는 행위 자체는 꽤나 익숙한 구조일 것이다.

          그럼 저 changeSegment 들을 사용하여 input 요소에 붙을 핸들러 중 segmentText 값을 수정하는 핸들러의 모습을 살펴보자

          changeSegment를 이용한 핸들러 예시
          const useCorrectionSegmentInput = ({
            paragraphOrder,
            segmentOrder,
          }: DiaryWriteSegmentInputProps) => {
            // 1. 기존의 Segment 를 구독
            const segment = useDiaryEditorStore(
              (state) => state.paragraphs.get(paragraphOrder).get(segmentOrder));
           
            const changeSegment = useDiaryEditorStore((state) => state.changeSegment);
           
            // 2. 기존 Segment의 식별자와 새로운 값을 이용해 업데이트 
           const handleChangeSegmentText = (text: string) => {
              changeSegment({
                clientSegmentId: segment.clientSegmentId,
                nextSegment: {
                  ...segmentState,
                  segmentText: text,
                },
              });
            };
          ...

          개인적으로 이 핸들러를 보며 느껴졌던 코드 스멜들을 회상해본다.

          1. 상태 변경 자체는 사실 테스트 하기가 어렵다

          우선 가장 크게 느껴졌던 부분은 리액트의 상태 관리 업데이트는 테스트 하기가 어렵다.

          예를 들어 호출 될 때 마다 인수의 값을 하나씩 증가시키는 순수 함수가 있다고 가정해보자

          이런 순수함수는 단순 호출 시 출력값이 입력값보다 1 큰지만 확인하면 된다.

          하지만 useState 와 같은 상태 값으로 정의되어 있는 로직이 잘 동작하는지 확인하기 위해선 상태 관리가 영향을 미치는 Virtual DOM -> Actual DOM 의 변화를 테스트 해야 한다.

          2. 상태 변경 로직이 인수의 인터페이스에 영향을 받는다.

          현재의 changeSegment 함수 자체는 변경하고자 하는 상태의 식별자와 변경 할 새로운 상태를 인수로 받는다.

          하지만 이런 경우를 생각해보자.

          만약 changeSegment 함수를 호출하는 부분에서 기존 정의된 식별자가 아닌 다른 식별자요소를 통해 식별하고 싶다면 어떻게 해야 할까?

          아마 다른 요소를 받는 핸들러를 또 생성해줘야 할 것이다.

          아마 이런 모양일 것이다.
          changeSegmentBySomething1 = ({something1 : ... , segment : Segment})=> void;
           
          changeSegmentBySomething2 = ({something2 : ... , segment : Segment})=> void;
           
          changeSegmentBySomething3 = ({something3 : ... , segment : Segment})=> void;

          이러한 일이 발생하는 이유는 changeSegment 라는 상태 변경 메소드가 인수 자체의 인터페이스에 크게 종속 되어 있는 구조이기 때문일것이다.

          그렇기에 받고자 하는 인수가 변경된다면 변경된 인수에 종속되는 다른 상태 변경 메소드를 또 만들어야 한다.

          즉, 상태 변경을 유발하는 컴포넌트들에선 다양한 요구 사항이 존재 할 수 있는데 상태변경 메소드 자체는 여러 요구사항을 만족 할 수 없는 구조이다.

          3. 현재 구조는 추상화 수준이 낮아 이해하기 어렵다.

          다시 상태 변경 메소드인 changeSegment 를 호출하는 부분을 다시 살펴보자

          가장 낮은 추상화 수준으로 정의된 핸들러
            const handleChangeSegmentText = (text: string) => {
              changeSegment({
                clientSegmentId: segmentState.clientSegmentId,
                nextSegment: {
                  ...segmentState,
                  segmentText: text,
                },
              });
            };

          추상화 수준이 가장 낮은 단계는 어떠한 유명 함수도 호출하지 않는방식이다.

          현재 changeSegment 의 인수에 들어가는 부분들은 단순 객체와 스프레드 문법으로만 작성 되어 있어 가장 낮은 추상화 수준으로 작성되어 있음을 알 수 있다.

          처음에는 이 문제를 해결하기 위해 changeSegment 의 인수를 만드는 메소드를 만들어 해결하고자 했었으나 이는 올바른 접근 방식이 아녔음을 깨달았다.

          처음 접근했던 잘못된 추상화 방식
          changeSegment(createChangeSegment(...))

          추상화 하고자 하는 요소는 어떤 요소를 찾아 어떤 형태로 수정 할 것인지 인데 현재의 방식은 단순 changeSegment 함수에서 기대하는 인수를 만드는 방식밖에 되지 않기 때문이다.

          함수형으로 바꿔보자

          구구절절 설명하기 전 변경된 형태를 먼저 보여주도록 한다.

          변경된 상태변경 메소드의 타입
          type Finder = (segment: Segment) => boolean;
          type Updater = (segment: Segment) => Segment;
           
          changeSegment: (finder: Finder,updater: Updater)

          이전과 다르게 인수를 객체가 아닌 함수들로 받도록 상태 변경 메소드가 수정했다.

          이를 통해 인수로 받을 인터페이스 구조에 종속되지 않고 해당 상태 변경 메소드를 호출하는 부분에서 제어 가능하게 되었다.

          상태 변경 메소드를 호출하는 부분
          import { hasSameClientId, updateSegmentId, updateSegmentText } from '../../lib';
           
          ...
           
          const useCorrectionSegmentInput = ({
            paragraphOrder,
            segmentOrder,
          }: DiaryWriteSegmentInputProps) => {
            const segment = useDiaryEditorStore(
              (state) => state.paragraphs.get(paragraphOrder).get(segmentOrder),
            );
            const changeSegment = useDiaryEditorStore((state) => state.changeSegment);
           
            const handleChangeSegmentText = (segmentText: string) =>
              changeSegment(hasSameClientId(segment), updateSegmentText(segmentText));

          이런식으로 말이다.

          이 리팩토링을 통해 무엇을 얻을 수 있었을까?

          1. 상태 변경 메소드를 호출하는 부분에서 상태 변경 로직을 제어 함으로서 더욱 유연한 구조를 가질 수 있다.
          2. 상태 변경 메소드를 호출하는 부분에서 인수를 유명 함수로 제공함으로서 추상화 수준을 높혀 어떤 로직들이 일어나는지 이해하기 쉬워졌다. (객체와 ... 로 범벅된 인수때와 비교해보자)
          3. 인수로 제공되는 함수들은 순수 함수로 구성되어 테스트 하기 매우 쉽다.
          매우 단순한 구조들의 순수함수 1
          import { Segment } from '@/shared/types/diary/shared/Editor';
           
          export const hasSameClientId = (segmentA: Segment) => (segmentB: Segment) =>
            segmentA.clientSegmentId === segmentB.clientSegmentId;
          매우 단순한 구조들의 순수함수 2
          import { Segment } from '@/shared/types/diary/shared/Editor';
           
          const updateSegmentWithPartial =
            (partialSegment: Partial<Segment>) =>
            (prevSegment: Segment): Segment => ({
              ...prevSegment,
              ...partialSegment,
            });
           
          export const updateSegmentText = (segmentText: string) =>
            updateSegmentWithPartial({ segmentText });
           
          export const updateSegmentId = (segmentId: string) =>
            updateSegmentWithPartial({ segmentId });
          note

          이 때 인수로 제공 될 함수들은 고계 함수 형태로 정의되어 있다.
          함수를 값처럼 이용하는 함수형 패러다임 자체에서 커링을 통한 고계함수는 일반적인 패턴이다.

          변경된 상태 변경 메소드의 구현체

          타입만으로 직관적이지 않을 수 있어 zustand 내부에서 구현된 코드를 함께 첨부한다.

          완전 완전 슈퍼 함수형으로 생각한다면 반복문을 재귀로 변경하고, mutable 한 isUpdate 변수 자체도 재귀 함수의 인수로 집어넣고 그래야겠지만 적절히 섞어 쓰도록 했다.

          changeSegment 구현체
                changeSegment: (finder, updater) => {
                  set(({ paragraphs }) => {
                    const newParagraphs = new Map(paragraphs);
                    let isUpdated = false;
           
                    for (const [paragraphOrder, segmentMap] of paragraphs) {
                      if (isUpdated) {
                        break;
                      }
           
                      for (const [segmentOrder, segment] of segmentMap) {
                        if (isUpdated) {
                          break;
                        }
           
                        if (finder(segment)) {
                          const updatedSegmentMap = new Map(
                            newParagraphs.get(paragraphOrder),
                          );
                          updatedSegmentMap.set(segmentOrder, updater(segment));
                          newParagraphs.set(paragraphOrder, updatedSegmentMap);
                          isUpdated = true;
                        }
                      }
                    }
           
                    return { paragraphs: newParagraphs };
                  });
                },
          • 함수형 패러다임이란
          • 코드스멜이 났던 기존 구조
          • 기존 구조의 코드 스멜을 알아보자
            • 1. 상태 변경 자체는 사실 테스트 하기가 어렵다
            • 2. 상태 변경 로직이 인수의 인터페이스에 영향을 받는다.
            • 3. 현재 구조는 추상화 수준이 낮아 이해하기 어렵다.
          • 함수형으로 바꿔보자
          • 변경된 상태 변경 메소드의 구현체

          abonglog

          공부한 내용을 기록하고 함께 성장하고 싶어 만든 두 번째 블로그입니다.
          주로 웹개발과 관련된 내용을 포스팅합니다.

          Githubttddcc119@naver.com

          © 2026 abonglog All rights reserved.

          이전 포스트이터레이터와 이터러블, 제네레이터, 비동기 이터러블
          다음 포스트모나드와 함께하는 함수형 프로그래밍 - Maybe 모나드