React 컴포넌트 설계 — AI와 함께 잡는 컴포넌트 구조

 


화면이 예뻐도 구조가 엉키면 끝이다

색상 팔레트는 완성했고, 별 배경은 흐르고, 카드 뒤집기 애니메이션은 매끄럽다. 78장의 이미지도 확보했다. 이제 이 모든 요소를 하나의 앱으로 조립해야 한다. 그런데 무작정 코드를 치기 시작하면 나중에 반드시 후회한다.

프론트엔드 개발에서 컴포넌트 구조는 건물의 골조와 같다. 외관이 아무리 멋져도 골조가 부실하면 새로운 기능을 추가하거나 버그를 수정할 때마다 벽이 무너진다. 특히 타로 앱처럼 여러 리딩 모드와 복잡한 상태 전이가 있는 프로젝트에서는 초기 구조 설계가 이후 개발 속도를 결정한다.

화면 흐름부터 정리하기

컴포넌트를 나누기 전에 사용자의 화면 흐름을 먼저 정리했다. 시작 화면에서 리딩 모드를 선택하고, 카드를 뽑고, 결과를 확인하는 세 단계다. 각 단계를 더 세분화하면 이렇다.

시작 화면에서 사용자는 네 가지 리딩 모드 중 하나를 선택한다. 원카드, 쓰리카드, 켈틱크로스, 자유선택. 모드를 선택하면 카드 선택 화면으로 이동한다. 여기서 뒤집힌 카드들 중에서 필요한 수만큼 카드를 고른다. 카드를 모두 고르면 뒤집기 애니메이션이 재생되고, 선택된 카드의 의미와 AI 해석이 표시된다.

이 흐름을 AI에게 설명할 때 중요한 것은 "화면"과 "데이터"를 분리해서 전달하는 것이었다. "시작 화면 → 카드 선택 → 결과 표시"라는 화면 흐름과 함께, "선택된 모드 → 뽑힌 카드 배열 → 각 카드의 위치와 방향 → AI 해석 텍스트"라는 데이터 흐름을 별도로 설명했다.

AI에게 컴포넌트 구조를 요청하는 팁

AI에게 "타로 앱의 컴포넌트 구조를 설계해줘"라고 바로 요청하면 너무 일반적인 결과가 나온다. App, Header, Footer, Card, Reading 같은 누구나 떠올릴 수 있는 구조가 제시된다. 이걸 쓸 수 있긴 하지만, 프로젝트의 구체적인 요구사항을 반영하지 못한다.

효과적인 요청 방법은 세 가지 정보를 함께 제공하는 것이다. 첫째, 화면 흐름을 구체적으로 설명한다. 둘째, 각 화면에서 필요한 데이터가 무엇인지 명시한다. 셋째, 화면 간에 공유되어야 하는 상태가 무엇인지 알려준다.

예를 들어 "카드 선택 화면과 결과 화면에서 동일한 카드 데이터를 사용해야 하고, 켈틱크로스 모드에서는 각 카드의 위치(position)가 해석에 영향을 미친다"는 정보를 제공하면 AI의 제안이 훨씬 구체적이고 유용해진다. 카드 위치를 포함한 타입 정의, 리딩 세션 상태 관리 구조까지 함께 제안해준다.

Context API로 리딩 세션 관리

상태 관리 도구 선택도 중요한 결정이었다. Redux, Zustand, Jotai 같은 외부 라이브러리를 쓸 수도 있었지만, React의 내장 Context API로 충분하다고 판단했다. 타로 앱의 상태는 복잡하지만 "전역적"이지는 않다. 하나의 리딩 세션 안에서만 공유되면 된다.

ReadingContext를 만들어서 리딩 모드, 선택된 카드들, 각 카드의 위치와 방향, AI 해석 결과를 한곳에서 관리했다. 새로운 리딩을 시작하면 Context가 초기화되고, 카드를 선택할 때마다 상태가 업데이트되며, 결과 화면에서 모든 데이터를 읽어간다.

AI와 논의하면서 유용했던 부분은 Context의 분리 전략이었다. 처음에는 하나의 거대한 Context에 모든 상태를 넣으려고 했다. AI가 "리딩 설정(모드, 카드 수)과 리딩 결과(선택된 카드, 해석)를 분리하면 리렌더링을 줄일 수 있다"고 제안했다. 실제로 모드 선택 화면에서 결과 데이터가 바뀔 일은 없으니 분리하는 것이 합리적이었다.

처음부터 완벽한 구조를 잡으려 하지 않기

컴포넌트 구조 설계에서 가장 빠지기 쉬운 함정은 처음부터 완벽한 구조를 만들려는 것이다. 아직 구현하지 않은 기능까지 예측해서 모든 가능성을 수용하는 유연한 구조를 만들려고 하면 과도하게 추상화된, 실제로는 사용하기 불편한 코드가 나온다.

이번 프로젝트에서는 의도적으로 "필요한 만큼만" 설계하는 접근을 취했다. 처음에는 원카드 모드만 동작하도록 구조를 잡았다. 카드를 하나 선택하고, 뒤집고, 해석을 보여주는 가장 단순한 흐름이다. 이 흐름이 완성된 후에 쓰리카드를 추가했고, 그 과정에서 구조가 자연스럽게 확장되었다.

AI에게도 이 접근을 명시적으로 전달했다. "지금은 원카드만 동작하면 됩니다. 나중에 쓰리카드와 켈틱크로스를 추가할 예정이지만, 지금 그것까지 고려하지 않아도 됩니다." 이렇게 범위를 명확히 하면 AI도 불필요한 추상화 없이 깔끔한 코드를 제안한다.

리팩토링은 실패가 아니라 자연스러운 과정

원카드에서 쓰리카드로 확장할 때, 예상대로 일부 컴포넌트의 구조를 변경해야 했다. 카드 하나만 보여주던 결과 화면이 세 장을 나란히 보여줘야 했고, 각 카드의 "위치"(과거-현재-미래)에 따른 해석 라벨도 필요했다.

이 과정을 "처음에 구조를 잘못 잡았으니 리팩토링이 필요하다"고 부정적으로 볼 수도 있다. 하지만 실제로는 반대다. 원카드로 동작하는 단순한 구조를 먼저 만들었기 때문에 "무엇을 바꿔야 하는지"가 명확했다. 처음부터 세 모드를 모두 고려한 추상적 구조를 만들었다면 오히려 수정이 더 어려웠을 것이다.

AI와의 리팩토링 작업도 효율적이었다. "원카드용으로 만든 이 컴포넌트를 쓰리카드에서도 쓸 수 있게 수정하고 싶다. 변경 사항은 카드가 배열로 들어오고, 각 카드에 위치 라벨이 필요하다"라고 설명하면 기존 코드를 기반으로 한 구체적인 수정안이 나온다. 기존 코드의 맥락을 이해한 상태에서의 수정 제안이라 실용적이다.

컴포넌트 네이밍에 쏟은 시간

사소해 보이지만 컴포넌트 이름 짓기에도 상당한 시간을 들였다. CardDisplay인가, CardView인가, CardReveal인가. 각각의 이름이 암시하는 역할이 다르다. Display는 정적인 보여주기, View는 좀 더 넓은 화면 단위, Reveal은 뒤집기 동작을 포함한다는 뉘앙스다.

AI에게 네이밍 대안을 요청하면 여러 옵션과 함께 각각의 뉘앙스 차이를 설명해준다. 최종 선택은 개발자의 몫이지만, 선택지를 넓혀주는 역할에서 AI는 유용하다. 혼자 고민하면 두세 개 후보에서 맴돌지만, AI와 논의하면 다섯 개 이상의 옵션을 빠르게 비교할 수 있다.

결국 화면 단위 컴포넌트에는 Screen 접미사를, 재사용 가능한 UI 요소에는 기능적 이름을, 레이아웃 관련에는 Layout 접미사를 쓰는 규칙을 정했다. HomeScreen, CardSpread, ReadingLayout 같은 식이다. 이 일관된 규칙은 파일 목록만 봐도 각 컴포넌트의 역할을 유추할 수 있게 해준다.

구조가 잡히면 속도가 붙는다

컴포넌트 구조를 정리하고 나니 이후 개발이 눈에 띄게 빨라졌다. 새로운 기능을 추가할 때 "이건 어느 컴포넌트에 넣어야 하지?" 라는 고민이 줄었다. AI에게 기능 구현을 요청할 때도 "ReadingLayout 컴포넌트 안에 카드 위치를 표시하는 라벨을 추가해줘" 같이 구체적으로 지시할 수 있게 되었다.

이 경험에서 얻은 교훈은 두 가지다. 첫째, AI와 협업할 때 구조 설계 단계에서 충분히 대화하는 것이 이후 시간을 크게 절약한다. 둘째, 완벽한 초기 설계보다 점진적 확장이 실제로 더 빠르고 안전하다. 두 번째 교훈은 AI 협업과 관계없이 모든 소프트웨어 개발에 적용되는 원칙이기도 하다.

다음 편 예고

컴포넌트 구조가 잡혔으니 이제 구체적인 레이아웃 구현에 들어간다. 다음 편에서는 이 프로젝트에서 가장 도전적이었던 레이아웃 과제를 다룬다. 바로 켈틱 크로스. 10장의 카드를 전통적인 십자형 배치로 화면에 배열하는 것을 CSS Grid로 어떻게 해결했는지, 그 과정의 시행착오와 발견을 공유한다.

댓글

이 블로그의 인기 게시물

사랑을 직접 올리지 않는 설계

감정을 변수로 옮기다 — 3계층 감정 모델

시작의 충동 — "타로 웹앱을 만들어볼까?"