추상화와 유연성은 양립할 수 있는가?

FrontendForm ValidationArchitectureRefactoringAbstraction

스프린트 미션 4는 바닐라 JS로 회원가입/로그인 인증 폼을 만들고, 입력 값을 검증해서 에러 메시지를 보여주는 과제였다.

처음엔 “폼 검증 로직을 잘 나눠서 구현해보자” 정도였는데, 제출 피드백을 받고 React로 마이그레이션까지 해보니 질문이 하나로 좁혀졌다.

추상화와 유연성은 함께 가져갈 수 있을까?

이 글은 내가 했던 설계가 왜 유연하지 않았는지, 그리고 그걸 어떤 관점으로 다시 정리했는지 기록한 내용이다.

내가 말하는 “추상화”와 “유연성”의 기준

이번 경험에서 애매했던 건 단어 자체가 아니라 “판단 기준”이었다. 그래서 기준부터 정리했다.

  • 추상화: 구현을 감추는 게 목적이 아니라, 변경이 일어나는 지점을 경계로 분리하는 작업
  • 유연성: 요구사항이 바뀌었을 때 수정이 한 지점에서 끝나고, 연쇄적으로 깨지지 않는 성질

즉, “설정으로 다 가능해 보이는 구조”가 유연한 게 아니라 “변경 비용이 국소화되는 구조”가 유연한 구조다.


1차 시도: 바닐라 JS에서 데이터로 폼을 생성하는 구조

요구사항 자체는 단순했다.

  • 회원가입 / 로그인 폼 구현
  • 각 input 값 검증
  • 실패 시 에러 메시지 출력

여기서 나는 “필드 정의만 있으면 렌더링/검증이 자동으로 따라오게 만들자”로 방향을 잡았다.
그래서 인증 필드를 데이터로 정의하고, 그 데이터를 기반으로 UI 생성 + 검증까지 처리하게 만들었다.

React (JSX)
DEFAULT_VALID_FIELDS = {
  index: number,
  class: string,
  description: string,
  discriptionStyle: string,
  isVisibility: Boolean,
  rules: [
    {
      element: string,
      innerContents: string,
      label: {
        innerContents: string,
        class: string,
      },
      attribute: {},
      options: [{ value: string, name: string }],
      checkValue: (value: string) => ({
        isValid: boolean,
        message: string
      })
    }
  ]
}[]

당시에는 “데이터 기반으로 재사용 가능하게 만들었다”라고 생각했다.

피드백: DEFAULT_VALID_FIELDS에 대한 의존성이 너무 강하다

제출 후 받은 핵심 피드백은 이거였다.

DEFAULT_VALID_FIELDS에 대한 의존성이 너무 강하다.

피드백을 내 코드 관점으로 풀면 이런 상태였다.

  • 폼 구조(UI), 검증 규칙(로직), 에러 메시지(UI), 검증 함수(비즈니스 로직)까지 전부 한 객체에 섞여 있다
  • 객체 구조가 조금만 바뀌어도 렌더링/검증/에러 처리 코드가 같이 흔들린다
  • 필드 하나 추가/수정이 “데이터만 바꾸면 끝”이 아니라, 결국 여러 코드가 함께 수정된다

내 의도는 “데이터로 유연하게 만들기”였는데, 실제론 하나의 데이터에 모든 책임을 몰아넣은 결합 구조가 됐다.

2차 시도: React로 옮기면 해결될까?

피드백 이후 “이 패턴을 React로 옮기면 나아지지 않을까?”라고 생각했다.

  • 이벤트 바인딩/상태 관리 등은 React가 더 안정적으로 처리한다
  • 필드 정의를 데이터로 관리하는 건 유지한다
  • 렌더링/리렌더링 관점에서 구조가 정리될 수 있다

그래서 바닐라 JS 아이디어를 React로 옮기면서 FIELDS 구조를 만들었다.

React (TSX)
FIELDS = {
  index: number;
  rules: {
    element: string;
    isVisibility?: boolean;
    ContainerAttribute?: {};
    label?: {
      contents: string;
    };
    attribute: {};
    checkValue: (value, allValues) => { isValid: boolean; message: string };
  }[];
}[];

React 컴포넌트는 이 데이터를 받아서

  • 어떤 요소를 만들지
  • 어떤 label을 붙일지
  • 어떤 속성을 부여할지
  • 어떤 규칙으로 검증할지

를 한 번에 처리했다.

결과: 문제가 그대로 반복됐다

React로 옮겼는데도 본질은 바뀌지 않았다.

  • FIELDS가 바뀌면 컴포넌트 곳곳이 같이 수정된다
  • 검증 규칙을 조금만 바꾸고 싶어도 FIELDS 구조부터 손대야 한다
  • 특정 필드(비밀번호)에만 필요한 UI 옵션까지 rules에 같이 들어가서 데이터가 점점 비대해진다
  • checkValue가 필드 정의에 묶여서 다른 곳에서 재사용하기 애매하다

정리하면 FIELDS는 “스키마”가 아니라 God Object에 가까워졌다.

  • UI를 어떻게 그릴지
  • 로직을 어떻게 검증할지
  • 특정 UI 기능(비밀번호 토글)까지

전부 한 덩어리로 뭉쳐 있었고, 그 결과는 “유연해 보이지만 변경에 약한 구조”였다.

내가 놓친 핵심: “데이터로 추상화” = “책임 분리”가 아니었다

멘토가 추천해준 키워드(OOP, Container/Presentational, 디자인 패턴, 3-Layers)를 따라가면서 공통점을 하나로 묶을 수 있었다.

  • 책임 분리
  • 계층 구조(단방향 의존성)

내 구조는 겉으론 분리돼 보였지만 실제론 계층이 없었다.

내가 만들고 싶었던 계층

  • 스펙(데이터) 레벨

    • “필드가 무엇인지”만 정의
  • 로직 레벨

    • “이 값을 어떻게 검증하는지”를 순수 함수로 관리
  • UI 레벨

    • “그걸 어떻게 보여주는지”에 집중

실제 내가 만든 계층(이상하게 섞인 상태)

  • FIELDS 안에 스펙 + 로직 + UI 옵션이 모두 들어감
  • 컴포넌트가 그걸 받아서 렌더링 + 검증 + 상태 처리까지 한 번에 수행함

분리는 “파일/상수” 기준으로 한 게 아니라 “책임/경계” 기준으로 해야 했는데, 나는 그걸 놓쳤다.

정리: 추상화는 경계를 만드는 일이고, 유연성은 변경을 국소화하는 성질이다

이번 경험에서 내가 얻은 결론은 이거다.

  • 추상화는 “데이터로 다 밀어 넣는 것”이 아니라, 변경이 일어나는 지점에 경계를 세우는 것이다
  • 유연성은 “어떤 것도 설정으로 가능하게 만드는 것”이 아니라, 수정이 필요한 순간에 연쇄 수정이 발생하지 않는 것이다
  • 올바른 추상화(경계가 있는 추상화)는 유연성을 떨어뜨리는 게 아니라 오히려 유연성을 만든다

다음에 내가 해볼 방향

다음엔 아예 구조를 이렇게 가져가려고 한다.

  • 필드 스펙은 “표현”만 가진다 (라벨, 타입, id 같은 최소 정보)
  • 검증 함수는 필드 밖으로 빼서 “순수 함수”로 둔다
  • UI는 범용 컴포넌트(Input, ErrorMessage 같은)로 나누고
  • Container에서 “스펙 + 검증 조합”을 구성한다

즉, 설정 하나로 만능을 만들기보다, 조합 가능한 단위들로 나눠서 확장하는 쪽으로 가보려 한다.

팀 프로젝트가 끝나면 스프린트 미션 6을 이어서 하면서, 이번에 정리한 “경계/계층/의존성” 관점을 코드에 더 명확히 녹여볼 생각이다.