본문으로 건너뛰기
면접 학습
복습세션 →

이력서 기반

토픽 1 · Phase 1

VAC 패턴 기반 3D 에디터 아키텍처

질문 5개
  1. Q1-1

    VAC 패턴이 뭐고 왜 도입하셨나요?

    핵심 포인트

    • 3D 엔진은 명령형 API (engine.setColor(matId, hex) 같은 호출형), React는 선언형(상태→뷰)이라는 패러다임 충돌이 출발점
    • VAC = View(순수 UI) / Action(엔진 명령 래퍼 훅) / Container(비즈니스 로직·상태 조합) 3계층
    • "엔진 API 스펙이 바뀌어도 UI 코드는 안 건드린다"는 것이 핵심 목표
    • MVP 기간 내 70개 이상 에디터 컴포넌트를 안정적으로 찍어내야 했던 제약이 설계의 동력
    모범 답안먼저 답해보고 펼치기

    3D 엔진 개발자분들이 만든 엔진 API가 명령형이었어요. engine.setMaterialColor(id, hex) 처럼 호출하면 뷰가 바뀌는 방식이죠. 반면 React는 선언형이라서 "상태가 이러면 화면은 이거다"가 자연스러운 사고예요. 이 두 패러다임을 그냥 섞으면 컴포넌트 안에 useEffect 가 명령형 호출로 가득 차고, 엔진 API가 한 번 바뀔 때마다 70개 컴포넌트를 다 뒤져야 하는 상황이 됩니다.

    그래서 세 계층으로 책임을 나눴어요. View는 진짜로 받은 prop을 그리기만 하는 dumb 컴포넌트, Action은 엔진의 명령형 API를 선언형 훅으로 래핑하는 어댑터 레이어, Container는 Atom 상태와 Action 훅을 조합해서 "지금 사용자가 무엇을 하려는가"라는 비즈니스 로직을 담당합니다. 이렇게 분리하니까 엔진 API 스펙이 바뀌어도 어댑터 훅 한 군데만 수정하면 됐고, 70개 넘는 에디터 UI는 영향 없이 계속 동작했어요.

    이 답변, 어땠나요?

    꼬리 질문
    • Q-a. VAC가 일반적으로 알려진 패턴인가요, 아니면 직접 명명하신 건가요?

      Container/Presentational 패턴(Dan Abramov)에서 영감을 얻었고, 거기에 "Action = 외부 명령형 API 어댑터"라는 레이어를 추가해서 팀 내부 명칭으로 정한 패턴입니다. 표준 GoF 패턴이라기보다는 어댑터 패턴 + Container/Presentational의 조합이라고 설명하는 편이 정확합니다.

    • Q-b. MVC, MVVM, Flux 같은 기존 아키텍처와 어떻게 다른가요?

      MVC는 View↔Controller 양방향이라 React에 잘 안 맞고, MVVM은 데이터 바인딩이 핵심이라 React 모델과 충돌. Flux/Redux는 단방향이지만 외부 명령형 엔진을 다루는 어댑터 계층 개념이 없습니다. VAC는 단방향(Container→Action→Engine→Atom→View) + 어댑터 패턴을 명시적으로 분리한 게 차이입니다.

    • Q-c. 70개 컴포넌트를 일관되게 만들 수 있었던 비결은요?

      Action 훅의 시그니처를 표준화했어요. 모든 엔진 훅이 useXxx(materialId) 형태로 selector + setter를 묶어서 반환하게 통일했고, 컨테이너에서는 그걸 그대로 갖다 쓰면 되도록 만들었습니다. 컨벤션을 코드 레벨에서 강제한 셈이에요.

    • Q-d. 테스트는 어떻게 짰나요?

      Action 훅은 엔진을 mocking 해서 단위 테스트, View는 storybook으로 시각 회귀, Container는 atom 초기값을 Provider로 주입해서 통합 테스트하는 식으로 계층별로 테스트 전략을 분리했습니다. (※ 실제 비중은 솔직하게 답변)

    CS · 이론
    • 명령형(Imperative) vs 선언형(Declarative) 패러다임
      • 명령형: "어떻게(How)" 단계별 지시 — canvas.fillRect(...), jQuery
      • 선언형: "무엇(What)"을 원하는지 기술 — React JSX, SQL
      • 두 패러다임을 섞으면 부수효과 추적이 어려워지므로 경계(boundary) 에 어댑터를 둔다.
    • 어댑터 패턴 (Adapter, GoF)
      • 호환되지 않는 두 인터페이스를 중간 객체가 변환해 주는 구조 패턴
      • VAC에서 Action 훅이 정확히 이 역할 — 엔진의 명령형 메서드를 React의 선언형 훅 인터페이스로 변환
    • Container/Presentational 분리 (Dan Abramov, 2015)
      • 본인이 후회한다고도 했지만 여전히 유효한 멘탈 모델
      • "데이터를 알고 있는 컴포넌트"와 "JSX만 그리는 컴포넌트"의 분리
    • OCP (Open/Closed Principle)
      • "확장에는 열려, 수정에는 닫혀" — 새 엔진 메서드 추가는 쉽고, 기존 UI 수정은 없어야 함
    • Anti-corruption Layer (DDD 용어)
      • VAC의 Action 레이어는 정확히 ACL 역할 — 외부 도메인(엔진)의 변화로부터 내부 도메인(UI)을 격리
  2. Q1-2

    View / Action / Container의 책임을 좀 더 구체적으로 나눠 주세요.

    핵심 포인트

    • View: prop in → JSX out, 그 외 어떤 사이드이펙트도 금지
    • Action: 엔진 메서드 1:1 래핑이 아니라, "한 번에 의미 있는 단위" 로 묶어서 노출
    • Container: Atom + Action을 조합해 "이 화면의 비즈니스 룰"만 담당, JSX는 거의 안 만진다
    • 의존성 방향: Container → Action / Atom → View (단방향)
    모범 답안먼저 답해보고 펼치기

    View는 정말로 dumb 합니다. props만 받고 JSX만 뱉어요. 컬러피커 UI라면 색상 배열과 onChange 콜백만 받고, 어떤 atom을 구독하거나 엔진을 호출하는 일은 일절 없어요. 그래서 storybook에서 prop만 바꾸면 모든 상태가 재현되고, 디자이너분들이 수정 요청을 줘도 View 파일만 보면 끝나요.

    Action 레이어는 엔진의 명령형 메서드를 선언형 훅으로 감쌉니다. 단순 1:1 래핑이 아니라 의미 있는 단위로 묶어요. 예를 들어 "소재의 컬러를 바꾼다"는 동작은 엔진 입장에서는 색 적용 + 미리보기 갱신 + 히스토리 푸시 세 단계인데, Action 훅에서는 useChangeMaterialColor() 하나로 묶어서 노출합니다.

    Container는 atom과 Action을 조합해서 "화면의 비즈니스 룰"만 들고 있어요. 예를 들어 "프리미엄 사용자만 컬러 시스템 X를 쓸 수 있다"는 정책은 Container 안에서 atom과 권한 데이터를 조합해 결정하고, 결과를 View에 props로 내려줍니다. 의존성 방향은 항상 Container → Action·Atom → View 단방향이어서, 역방향 의존이 생기면 ESLint 룰로 막았습니다.

    이 답변, 어땠나요?

    꼬리 질문
    • Q-a. View에 useState 같은 내부 상태도 못 쓰게 하셨나요?

      폼 입력값처럼 정말 UI 한정 상태는 허용했지만, "다른 컴포넌트가 알아야 하는 상태"는 무조건 atom으로 끌어올렸습니다. 기준은 "화면 밖에서도 의미가 있나"였어요.

    • Q-b. Container가 너무 비대해지지 않나요?

      화면 단위로 한 개 두기보다, 의미 단위로 작게 여러 개 쪼갰어요. 컬러 편집 컨테이너, 텍스처 편집 컨테이너처럼요. Container 자체가 비대해지면 도메인 분리 신호로 보고 분할했습니다.

    • Q-c. 의존성 방향을 어떻게 강제했나요?

      eslint-plugin-import의 no-restricted-paths 룰로 View → Container, View → Atom 임포트를 금지했습니다. PR 리뷰 단계에서 Lint로 막혔어요.

    CS · 이론
    • Hexagonal Architecture / Ports & Adapters
      • 비즈니스 코어(Container)를 외부 의존(엔진, 네트워크, UI 라이브러리)으로부터 격리
      • VAC의 단방향 의존이 정확히 이 사상
    • 단방향 데이터 흐름 (Unidirectional Data Flow)
      • React/Flux/Redux 공통 사상. 디버깅 가능성과 시간 추적 용이성이 핵심 가치
    • 단일 책임 원칙 (SRP)
      • View는 그리기, Action은 어댑팅, Container는 정책 — 각자 한 가지 이유로만 변경됨
  3. Q1-3

    명령형 엔진 API를 선언형 훅으로 어떻게 감쌌나요? 구체 예시를 들어 주세요.

    핵심 포인트

    • 명령형 호출의 결과를 atom에 미러링해서, "엔진 = source, atom = projection" 구조로 만듦
    • 훅 내부에서 useEffect로 엔진 이벤트를 구독, atom을 업데이트
    • 사용자 액션(setter)은 atom과 엔진을 동시에 갱신 (또는 엔진 이벤트로 단방향 동기화)
    • 일관된 시그니처 — [value, setValue] = useXxx(matId)
    모범 답안먼저 답해보고 펼치기

    가장 흔한 패턴이 "엔진을 source of truth로 두고 atom은 그걸 미러링한다"는 구조였어요. 예를 들어 useMaterialColor(matId) 라는 훅을 만들면, 내부에서 두 가지 일을 합니다.

    첫째, 마운트될 때 엔진의 컬러 변경 이벤트를 구독해서 atom에 푸시해요. 그러면 다른 컴포넌트가 같은 atom을 구독하고 있다가 React 식으로 자동 리렌더링됩니다. 둘째, setter를 호출하면 atom 값을 업데이트하고 동시에 엔진 명령(engine.setMaterialColor)을 호출합니다. 그러면 엔진 이벤트가 다시 돌아와도 같은 값이라 무한 루프 없이 멈춰요.

    훅 시그니처를 useState 와 똑같이 [value, setValue] 형태로 통일한 게 포인트예요. 동료 분들이 "엔진을 다룬다"는 사실을 신경 쓸 필요 없이, 그냥 useState 처럼 쓰면 화면이 바뀌고 엔진도 바뀌는 경험을 하게 만들었습니다. DX 면에서 학습 비용이 거의 0에 수렴했어요.

    이 답변, 어땠나요?

    꼬리 질문
    • Q-a. atom과 엔진 사이 동기화가 깨지면 어떻게 처리하나요?

      엔진 이벤트를 단일 source로 두고, setter도 엔진을 거쳐 atom을 갱신하는 흐름으로 만들면 한 방향만 신뢰하면 됩니다. 양방향이 필요할 때는 dirty flag를 atom에 두고 충돌 시 엔진 우선으로 resolve 했습니다.

    • Q-b. 무한 루프나 race condition 위험은 없었나요?

      같은 값으로의 setter 호출은 엔진 레이어에서 no-op 처리하고, race가 생기는 비동기 텍스처 로딩은 인플라이트 캐시(토픽 3 참고)로 막았습니다.

    • Q-c. 엔진 이벤트가 너무 자주 발생하면 React 리렌더가 폭주하지 않나요?

      빈도 높은 이벤트(드래그 중 컬러 변경)는 RAF 스로틀로 묶어서 atom에 반영했고, atomFamily로 인스턴스를 쪼개서 영향 범위도 최소화했습니다.

    CS · 이론
    • Pub/Sub 패턴: 엔진 이벤트 구독은 전형적인 publisher-subscriber 모델
    • Mirroring / Projection: 외부 system 상태를 내부 store에 단방향으로 투영하는 아키텍처 기법
    • React useSyncExternalStore (React 18+): 외부 store와 React를 안전하게 연결하는 표준 훅. tearing 없이 concurrent rendering에서도 일관된 값을 보장
    • Tearing: 같은 store 값을 동시에 두 컴포넌트가 다르게 보는 현상. concurrent rendering 도입 후 React가 신경 쓰는 문제
  4. Q1-4

    AtomFamily를 쓰셨다고 했는데, 어떤 문제를 해결한 건가요?

    핵심 포인트

    • 소재(Material)가 수십~수백 개, 각자 독립적으로 편집되어야 함
    • 단일 atom + Map<id, value> 으로 만들면 한 소재 변경 시 전체 트리 리렌더 발생
    • atomFamily는 id 키마다 독립 atom 인스턴스를 만들어주므로, 변경된 atom 구독자만 리렌더
    • "해당 컬러칩 컴포넌트만 리렌더된다"는 것이 핵심 결과
    모범 답안먼저 답해보고 펼치기

    3D 에디터에서 소재가 많아요. 한 화면에 컬러칩이 수백 개 떠 있고, 각자 독립적으로 편집됩니다. 처음에는 단일 atom으로 Record<MaterialId, MaterialState> 같은 큰 객체를 들고 있었는데, 한 소재의 색깔만 바꿔도 객체 참조가 바뀌니까 그 atom을 구독하는 모든 컴포넌트가 리렌더됐어요. 화면 단위 frame drop이 보일 정도였습니다.

    Jotai의 atomFamily는 id 같은 인자로 atom을 캐싱해서 만들어 줍니다. materialColorAtom('matA')materialColorAtom('matB') 가 서로 다른 인스턴스가 되니까, matA 색을 바꾸면 matA를 구독하는 컴포넌트만 리렌더돼요. 트리 전체가 흔들리던 게 정확히 그 칩 한 개만 흔들리는 형태로 바뀌었습니다.

    부수 효과로 selector 코드가 깔끔해졌어요. 기존엔 큰 객체에서 state.materials[id].color를 뽑아내는 selector를 매번 만들어야 했는데, 이제는 그냥 atom을 호출하면 되니까요.

    이 답변, 어땠나요?

    꼬리 질문
    • Q-a. atom이 너무 많이 만들어지면 메모리 누수 위험은요?

      atomFamily는 내부적으로 weak 참조가 아니라 일반 Map으로 캐싱해서, 소재가 삭제될 때 명시적으로 materialColorAtom.remove(id)를 호출해 줘야 합니다. 소재 라이프사이클 훅에서 cleanup으로 호출하도록 했어요.

    • Q-b. AtomFamily 없이 selector만으로 같은 효과를 낼 수 있지 않나요?

      가능은 합니다. 하지만 selector 기반이면 "큰 atom의 일부를 잘라서 본다"는 사고이고, 큰 atom 자체가 변경 알림 단위라서 React-Redux의 useSelector와 같은 reference equality 비교 비용이 듭니다. atomFamily는 변경 알림 자체가 쪼개져 있어서 비교 비용 자체를 없애 줘요.

    • Q-c. 같은 효과를 Recoil로도 낼 수 있는데 굳이 Jotai를 선택한 이유는요?

      Recoil도 atomFamily가 있지만, atom key를 문자열로 강제해서 충돌 위험이 있고 메인테이너 지원이 위축됐습니다. Jotai는 atom 자체가 객체 참조라 key 문자열이 필요 없어요. 자세한 내용은 마이그레이션 토픽에서 다룹니다.

    CS · 이론
    • AtomFamily 패턴: 매개변수에 의해 캐싱되는 atom factory. Recoil/Jotai 공통 개념
    • Reference equality와 React 리렌더: React는 Object.is로 prop/state 변경을 판단. 객체 참조가 바뀌면 무조건 리렌더 후보
    • Memory leak 패턴: 동적으로 만든 atom·subscription·timer 는 명시적 cleanup이 필수
    • Selector pattern (Reselect): 큰 store에서 부분을 잘라보는 보편적 기법. atomFamily는 그 변종에 가까움
  5. Q1-5

    다중 컬러 시스템(Picker / Pantone / Coloro)을 Strategy 패턴으로 통합했다고 하셨는데, 어떤 문제를 풀었나요?

    핵심 포인트

    • 컬러 시스템마다 입력 형식·검색 방식·미리보기 규칙이 모두 다름
    • 컴포넌트 안에서 if/else 분기로 처리하면 시스템 추가 시 매번 컴포넌트 수정 (OCP 위반)
    • Strategy 패턴: 공통 인터페이스(ColorSystem)를 정의하고, 각 시스템을 구현체로 분리
    • Context API에 현재 strategy를 주입 → 컴포넌트는 strategy.method()만 호출
    모범 답안먼저 답해보고 펼치기

    컬러 시스템이 세 가지였는데 입력·검색·렌더링 규칙이 다 달랐어요. Picker는 RGB 슬라이더, Pantone은 코드 검색, Coloro는 5자리 숫자 코드처럼요. 처음엔 컴포넌트 내부에서 if (system === 'pantone') { … } 로 분기했는데, 새 시스템(예: HKS)을 추가할 때마다 모든 분기 지점을 찾아 수정해야 해서 부담이 컸어요.

    Strategy 패턴을 적용했어요. ColorSystemStrategy 라는 공통 인터페이스를 정의하고 — search(query), parse(input), format(color), swatches() 같은 메서드 — 각 시스템을 구현체로 따로 만들었습니다. 컬러 편집 컴포넌트는 Context로 현재 strategy를 받아서 그냥 strategy.search(q)만 호출해요. 새 시스템을 추가할 때 구현체 파일 하나 만들고 Context에 등록만 하면 되고, 기존 컴포넌트 코드는 한 줄도 안 건드립니다. OCP가 그대로 만족됐어요.

    이 설계 덕에 디자이너분들이 컬러 시스템을 늘리자고 제안할 때 "1주 안에 가능합니다"가 자연스럽게 나오는 환경이 됐고, 실제로 신규 시스템 추가는 평균 2~3일 안에 끝났습니다.

    이 답변, 어땠나요?

    꼬리 질문
    • Q-a. Strategy 대신 if/else나 lookup map을 써도 되지 않나요?

      Lookup map도 같은 사상의 단순한 변종이지만, 시스템마다 메서드가 4~5개씩이라 map 안에 함수가 중첩되어 복잡해집니다. Strategy 인터페이스로 묶으면 TypeScript가 메서드 시그니처를 컴파일 타임에 강제할 수 있어 안전성에서 우위입니다.

    • Q-b. 새 컬러 시스템이 기존 인터페이스에 안 맞으면요?

      그게 인터페이스를 다시 봐야 한다는 신호입니다. 실제로 한 번 인터페이스를 한 번 더 추상화한 적이 있어요. 변하지 않는 핵심(예: search) 과 옵셔널 확장(예: gradient 지원)을 분리했습니다.

    • Q-c. Context API 대신 Provider props/HOC로 strategy를 주입할 수도 있는데 왜 Context를 골랐나요?

      여러 깊이의 컴포넌트가 공유해야 했고, Compound Component 패턴과 자연스럽게 결합했어요. Provider props로 내리면 prop drilling이 심해졌을 거예요.

    CS · 이론
    • Strategy 패턴 (GoF): 알고리즘을 인터페이스 뒤로 추상화해서 런타임에 교체 가능하게 함
    • OCP (Open/Closed Principle): 확장에 열려있고 수정에는 닫혀있어야 함. Strategy 패턴은 OCP의 전형 사례
    • DI (Dependency Injection): Context API를 통한 strategy 주입은 React식 DI
    • TypeScript interface vs discriminated union: strategy를 표현할 때 두 선택지 비교 가능. 메서드가 많으면 interface, 데이터 형태 분기면 discriminated union