티스토리 뷰
"복잡한 UI, 우아하게 풀어내기"를 주제로한 토스 프론트엔드 다이빙클럽 6번째 모임에 참여했다. 세션 구성은 다음과 같았다:
- 복잡한 폼을 다루는 기술: Multi-Step Form부터 대규모 입력 관리까지 (토스 Frontend Developer 박종호)
- 바닥부터 직접 설계하는 그리드 레이아웃 UI (토스증권 Frontend Developer 박건영)
발표 내용을 듣고 스몰 토크를 나누는 과정에서 많은 것들을 느꼈다.
원래는 프다클 참여 후기를 세세하게 작성할까 했지만, 너무 대단한 내용을 들어서 "이걸 내가 온전히 전달할 수 있을까" 하는 생각과 "이런 내용을 공유해도 괜찮을까" 하는 고민을 하다 그러지 못할거 같다는 생각을 하게 되었다.
그래서 그냥 이 발표들의 내용을 간략하게 정리하고 내가 느낀 바를 공유한 후 나는 내가 마주한 구현의 어려움을 어떻게 해결했고 앞으로의 계획은 어떻게 되는지를 공유해보려 한다.
복잡한 폼을 다루는 기술: Multi-Step Form부터 대규모 입력 관리까지 (토스 Frontend Developer 박종호)
프론트엔드에서는 복잡한 Form을 다룰 일이 많다. 다만 나는 그래본 경험이 없다... 견문이 부족하고 평소에도 고민하고 되새김질을 해야만 내용이 체화되는 경우가 많았다(그래서 인강이나 책을 좋아한다).
이번에도 사실 그랬다. 개발 얘기를 좋아하고 많이 나누고 싶었지만, 견문이 부족해 발표 내용이 바로 온전히 받아 들여지지 않았다. 발표 이후 토론 시간이 주어졌는데 고민과 대화를 번갈아 하다보니 많은 얘기를 하지 못한것 같아 너무 아쉬웠다.
발표 내용은 기존의 중앙 집중화 방식에서 복잡한 폼을 다룰때 발생하는 문제를 어떻게 해결했는지에 대한 내용이었다. Top-Down 형식의 Form 데이터 관리를 Bottom-Up 형식으로 개선한 얘기였는데 이걸 조직 구조에 빗대어 표현한게 흥미로웠다.
일반적으로 조직에서의 의사 결정 흐름은 크게 두 가지 방식이 될 수 있다고 생각한다:
1. Top-Down 방식
2. Bottom-Up 방식
각 방식의 차이점에 대해 간략하게 알아보자.
Top-Down 방식

Top-Down 방식은 의사 결정이 위에서 아래로 전달된다. 주요한 특징은 다음과 같다:
1. 의사결정 흐름
- 최고경영진이 전략적 의사결정을 내림
- 중간관리자가 이를 구체적인 실행 전략으로 변환
- 실무자들은 정해진 계획에 따라 실행
2. 핵심 요소
- 중앙집중적 통제: 의사결정 권한이 상위층에 집중
- 명확한 지휘체계: 책임과 권한이 명확하게 구분
- 표준화된 프로세스: 일관된 실행과 통제가 가능
3. 장점
- 신속한 의사결정과 실행이 가능
- 일관된 전략 실행이 용이
- 책임소재가 명확함
4. 단점
- 현장의 의견 반영이 어려움
- 환경 변화에 대한 대응이 늦을 수 있음
- 구성원의 자율성과 창의성이 제한될 수 있음
이러한 하향식 구조는 특히 대규모 조직이나 안정적인 환경에서 효과적으로 작동할 수 있다. 예를 들어, 제조업이나 금융기관과 같이 표준화된 프로세스가 중요한 산업에서 자주 활용된다.
Bottom-Up 방식

Bottom-Up 방식에서는 의사 결정이 아래에서 위로 보고된다. 주요한 특징은 다음과 같다:
1. 의사결정 흐름
- 실무자들이 현장에서 아이디어를 제안
- 중간관리자가 의견을 수렴하고 체계화
- 최고경영진이 검토하고 최종 의사결정
2. 핵심 요소
- 분산된 의사결정: 현장의 자율성 존중
- 수평적 소통: 자유로운 의견 교환과 협력
- 유연한 프로세스: 상황에 따른 신속한 대응
3. 장점
- 현장의 실질적 문제해결이 용이
- 구성원의 참여도와 만족도가 높음
- 혁신적인 아이디어 도출이 활발
4. 단점
- 의사결정 시간이 길어질 수 있음
- 전사적 일관성 유지가 어려울 수 있음
- 조정과 합의에 많은 노력이 필요
이러한 상향식 구조는 특히 기술 기반 기업이나 창의성이 중요한 산업에서 효과적이다. 예를 들어, IT 기업이나 스타트업과 같이 빠른 혁신과 변화가 필요한 조직에서 많이 활용된다.
조직 구조로 보는 데이터 구조

전체적으로 정리해보면 위 사진과 같다. 이를 Form 데이터 구조에 대입해보자:
핵심은 Top-Down 방식의 Form 구조에서는 최상위 계층에 Validation, Setter 이외의 모든 비즈니스 로직이 작성되어 하위로 전달되고 하위에서는 자신이 해야할 동작과 표현할 값을 전달받아 보여주고, 실행하기만 한다.
Bottom-Up 방식에서는 각 계층이 각자의 할 일을 가지고 비즈니스 로직을 가진다. 이 로직들을 실행하고, 실행한 결과를 상위 계층에서 활용한다.
그러니 Top-Down에서는 중압 집중적인 구조가 따라오고, Bottom-Up 방식에서는 독립된 구조가 따라오게 된다.
Top-Down 방식의 문제점과 Bottom-Up의 해결 방식

Form은 여러개의 Step으로, Step의 여러개의 필드로 구성된다. Top-Down 방식에서 이러한 Form을 구현한다면 다음과 같은 로직들이 최상단에 포함되게 된다:
- 전체 Form에 대한 스키마
- 전체 Form에 대한 Setter
- 각 Step에 대한 Validation
- 각 Field에 대한 Validation
...
이러한 상태에서 여러 비즈니스로직이 추가적으로 더해진다면?
예를 들어 lasName, firstName이라는 필드가 각각 존재하지만 Form에서는 name으로 존재해야 하는 상황이라면? 계속해서 로직이 쌓이고 최상위 계층에 존재하는 비즈니스 로직이 비대해지게 된다.
발표자이신 종호님은 이런 문제를 만났고 이를 해결하기 위해 Bottom-Up 방식을 적용했다고 하셨다.

Bottom-Up 방식은 위처럼 구성된다. 각 Field가 각자의 Validation, Getter, Setter를 가지고 이 결과값을 자신에게 저장한다. 각 Step은 Field들의 처리 결과를 바탕으로 또 다른 로직을 실행하고 이 결과를 자신에게 저장한다. Form은 각 Step들의 처리 결과를 바탕으로 완성된 페이로드 및 모든 Step의 유효성 여부를 판단한다.
이렇게 하면 Form 최상위 계층의 몸집이 거대해 지는것을 막을 수 있고 각 Step, Field가 독립된 환경을 만들어 확장성을 얻을 수 있다.
Trade off는?
발표 이후 스몰토크에서도 나눴던 얘기지만, Trade off는 무엇일까?
Top-Down 방식은 구현이 비교적 간단하다. 비즈니스 로직과 모든 Form에 대한 데이터가 함께 존재하는 상황이기 때문에 간단하게 구현할 수 있다.
다만 결합도가 높다는 문제가 있다. 특정 로직을 수정해야 할 때 다른 로직도 수정해야할 수 있다. 또한 최상위 계층인 Form에게 너무 많은 책임이 주어진다. 모든 Form에 대한 Validation과 Setter부터 모든 각 필드에 대한 Validation을 가지고 있어야 한다.
Bottom-Up 방식은 독립적이다. 각 Field, Step, Form이 각자의 독립적인 책임을 가지고 있다. 그러므로 결합도가 낮다. 특정 로직을 수정해야 한다면, 필요한 부분만 수정할 수 있다.
다만 구현이 Top-Down 방식에 비해 복잡하다는 문제가 있다. 또한 독립적이기에 다른 값에 의존해야 하는 경우에 대응이 어렵다.
내 생각
버스타고 돌아오면서 이 발표에 대한 생각을 많이했다. 그리고 글로 적으니 확실히 어떤 문제를 해결하려 했고, 어떤 장점이 있을지가 조금 더 명확해진 느낌이다.
다만 독립적이기에 다른 값에 의존하는 경우에 대한 해결책이 필요하다는 생각이 들었고, DI 패턴처럼 필요한 의존성을 상위 계층에서 주입해 줄 수 있는 방법은 없을까? 같은 것들을 고민했다.
바닥부터 직접 설계하는 그리드 레이아웃 UI (토스증권 Frontend Developer 박건영)
토스 증권에서 사용되는 그리드 패널에 관한 얘기였다. 요약하면 아래 사진처럼 구성될 수 있는 그리드 패널을 만들어야 했다는 얘기였다:
이 기능에 대해서는 토스증권 페이지에서 확인할 수 있다.
토스 증권의 건영님은 이 UI에서 이진 트리의 동작 방식을 발견했다.
이 이진 트리의 동작 방식을 활용해 그리드 패널을 구현했고, 모두가 만족하는 성과를 단기간에 구현해 냈다고 하셨다.
내 두뇌로 이정도 설명이 한계다... 이외에도 패널의 이동같은 연산도 이진 트리 연산을 적용해 해결하셨다고 한다!
내 생각
사실 설명할게 많이 없다. 그만큼 간결하게 임팩트 있는 내용이었다. 이 내용을 곱씹으며 들었던 생각은, 여태 나는 프론트엔드에서 UI를 구성하는 일과 데이터를 다루는 일을 분리해서 생각해 왔다는 점이다.
프론트엔드는 데이터를 UI로 표현한다. 결국 이 복잡한 UI를 어떻게 구현할것인가 라는 얘기는 복잡한 데이터를 어떻게 다룰것인가와 다르지 않겠다는 생각이 들었다. UI를 데이터로 바라보는 시각을 키워야 겠다는 생각을 했다.
밑바닥부터 데이터 테이블 구현했던 일
취준생때 자산 관리 웹사이트 사이드 프로젝트를 시작했고, 취업에 성공한 지금도 계속해서 개발하고 있다. 오프라인 UT도 진행하고, 유저 피드백을 바탕으로 개선작업도 진행했다. 이제는 곧 출시를 앞두고 있다.
이 프로젝트에서 아래와 같은 데이터 테이블을 구현하는 작업을 맡게 되었다.

이 데이터 테이블을 라이브러리 없이 구현한 이유는 2가지 였다:
- 한번쯤은 정말 어려운 도전적인 문제를 해결해보고 싶었다.
- 디자인 요구사항을 만족시키는게(리사이즈 핸들러 같은) 오히려 직접 구현 하는게 더 낫겠다고 생각했다.
실제로 구현하면서 다양한 문제를 만났다.
첫 번째 문제: 열과 행 무엇을 기준으로 블록을 쌓는게 좋을까?
테이블이라는게 여러 셀들이 모여 열 또는 행을 구성하고, 열 또는 행이 쌓여 테이블이 된다. 그럼 이때 열과 행 무엇을 기준으로 쌓아나갈 것인가에 대한 고민이 생겼다.
이는 행을 기준으로 쌓기로 결정했다. 내가 구현한 데이터 테이블에 딱 하나 의존하는 외부 라이브러리가 있는데, 그게 ShadCN의 리사이즈 패널이다. 이를 활용하기에 행을 쌓는 방식이 좋다고 생각해서 그렇게 했다. 뿐만 아니라 테이블의 너비가 기본 100% 이지만 각 필드의 최소 너비의 합이 윈도우의 너비보다 넓어지면 가로 스크롤이 생겨야 한다. 이런 너비 기반 동작 방식을 적용하기에도 행 단위의 블록이 적합하다고 판단했다.
두 번째 문제: 다양한 형태의 셀을 어떻게 구현해야 할까?
두 번째로 각 셀의 형태가 정말 다양한데 이를 어떻게 가능하게 할것인가 였다. 이는 간단하게도 builder 함수를 노출시켜 해결했다. 어느정도 비즈니스 모델과 결합도를 높인 형태로 구현하고 있었기에 테이블 자체의 모양이나, 셀 자체의 틀은 강제해도 괜찮았다. 그래서 정해진 틀 안에서 내부 내용을 자유롭게 적용할 수 있도록 인터페이스를 노출시켰다.
세 번째 문제: 라사이즈 핸들러, 드래그 앤 드롭 인디케이터를 어떻게 디자인 요구사항에 맞게 개발할지
세 번째로 리사이즈 핸들러나, 드래그 앤 드롭시 삽입될 위치를 표현하는 인디케이터를 어떻게 구성할지 였다. 이 문제는 사실 열과 행 어떤걸 기준으로 할지에 대한 의사결정에도 많은 영향을 줬다. 각 행 블록 사이 어디에 들어갈지를 표기하는 인디케이터이고, 이는 곧 각 행 블록 사이의 디바이더라 생각할 수 있겠다는 판단을 했다. 그래서 각 행 사이에 디바이더를 만들고 이 디바이더가 인디케이터 역할도 하게끔 구현했고, 리사이즈 핸들러도 이와 유사한 판단으로 가져가서 해결했다.
마지막 문제: 추상화 수준을 얼마나 가져가지?
어느정도의 인터페이스를 노출하고, 어느정도 커스터마이징이 가능하게끔 할것인지에 대한 고민이었다. 이는 사실 처음부터 결정했던 사항이라 크게 문제는 되지 않았다. 우리의 비즈니스 모델에 결합된 형태로 개발했다. 예를들어, 내가 만든 데이터 테이블에서 리사이즈, 재정렬 등의 기능은 기본 기능이며 핸들러 인디케이터의 디자인 역시 변경 불가능하다. 이렇게 개발해야 일정에 맞는 개발이 가능하다 판단했고, 라이브러리를 사용하지 않은것에 대해 리소스 효율적인 성과를 낼 수 있다고 생각했다.
이렇게 해서 데이터 테이블을 밑바닥 부터 구현하는데 성공했다(리사이즈를 제외한...). 다만 이번 발표를 듣고 개선할 부분들을 많이 떠올리게 되었다.
Table의 구조 개선하기 - 고민
현재 내가 구현한 Table은 외부에 비즈니스 로직이 모두 존재한다. 외부에서 -> Table Column -> Cell로 해야하는 일들을 전달해주고 있다. 이걸 Bottom-Up 방식으로 개선하면 어떨까? 하는 생각을 했다.
다만 Bottom-Up 방식의 장점은 각 계층을 독립적으로 만들 수 있음에 있다고 생각하고 Table에서는 Column은 Table에, Cell은 Column에 의존할 수 밖에 없기에 큰 도움이 되지 않을거라는 판단을 내리게 되었다. 특정 Cell이 어디에 있는 Cell인지 알기 위해서는 열의 Id와 어떤 Field의 칼럼인지를 알아야 하기 때문이다.

그렇다면 중앙 집중화된 방식에서 오는 단점을 어떤 방식을 통해 개선할 수 있을까?
Table의 구조 개선하기 - 데이터로 생각하기
결국 테이블의 형태는 데이터의 형태와 다르지 않다. 특정 필드를 기준으로 정렬하는 것은 배열의 순서를 변경하는 것이고, 어떤 열을 확장하고 닫는것은 배열의 특정 요소를 추가하고 제거하는 작업이라 볼 수 있다.
그래서 테이블 외부에서 데이터를 가공해서 전달하던 방식에서 reorder, sorting, expand 같은 기능들을 커스텀 훅 내부로 옮기고 Public API의 형태로 만들어 보려 한다. 다음과 같은 형태가 될거같다:
const { sortBy, expandRow, columns, dataSet } = useTableAPI(parsedData);
sortBy("매매일자", "desc");
expandRow((row) => row.id === 0);
Table의 구조 개선하기 - 추상화 수준 높이기
현재는 외부에서 데이터를 가공해서 전달하면 Table에서 이를 토대로 그리는 작업만 담당하고 있고, onResize, onReorder등의 인터페이스를 노출해 외부에서 이를 처리하게끔 처리해뒀다. 그렇기에 "중앙"에서 해야하는 비즈니스 로직이 점점 늘어나며 비대해지는 중이다. Table의 추상화 수준을 높여서 책임을 명확히 분리하고, 내부에서 처리할 수 있게 하려고 한다.
그리고 이를 콜백을 통한 인터페이스로 노출시키는게 아닌 선언적인 방식으로 전달받을 수 있게 하려고 한다:
<Table
columns={[
{
displayName: "매매일자",
accessKey: "매매일자", // 이 칼럼에 속한 데이터에 접근하기 위한 키
reOrderable: true,
resizable: true,
required: true,
...
}
]}
dataSet={
"매매일자": "2024-02-01",
"수량": 5,
...
}
headerBuilder={(key) => <Header field={key} />
cellBuilder={(rowId, field, value) => <Cell rowId={rowId} field={field} value={value} />
/>
이정도로 테이블의 기본적인 동작(리사이즈, 재정렬, 확장 등등)의 기능은 내부로 감추고, 각 셀이 어떤 UI를 표현해야 하며 어떻게 동작해야 하는가는 외부에서 주입받을 수 있게끔 하려고 한다. 이렇게 했을 때 아래처럼 수정 포인트를 간편하게 찾아낼 수 있을거라 생각했다:
- 리사이즈, 필드 기준 정렬, 필드 자체 재정렬, 확장의 기능 즉 테이블 자체의 기능이 동작하지 않는다. -> 테이블에서 문제가 생겼다.
- 각 셀의 기능이 제대로 동작하지 않는다. -> 주입하는 Cell에 존재하는 로직에서 수정할 수 있다.
이렇게 하면 어느정도 Table과 Cell의 동작을 독립적으로 분리하고 관리할 수 있겠다는 생각이 들었다. 있어보이게 말하면 중앙 집중형 방식에 존재할 수 있는 의존성이나 강결합의 문제를 DI 패턴으로 해결했다...? 라고 설명할 수 있는건가 싶기도 하다.
결론
사실 작년 3월 상경하고 생애 첫 개발자 컨퍼런스 참여였다. 정말 많은곳에 지원했지만 번번히 떨어졌기 때문이다. 그만큼 기대감 한가득, 긴장감 한가득 안아들고 갔고 정말 많은 인사이트와 고민거리를 가지고올 수 있었다. 회사에서 바로가면 시간이 애매해서 연차쓰고 갔는데 그게 아깝지 않았다. 다음에도 참여할 기회가 있다면 참여해서 고민거리를 많이 가져오고싶다.
'개발' 카테고리의 다른 글
| 프로토타입 A to Z (0) | 2026.07.19 |
|---|---|
| 응집도와 결합도 (0) | 2026.07.19 |
| Dialog, Modal, Sheet의 차이 알고 사용하기 (0) | 2026.07.19 |
| 멀리 보기위한 타입스크립트 (0) | 2026.07.19 |
| 포트폴리오 사이트에 SEO 적용하기 (Next.js) (0) | 2026.07.19 |
