Skip to content

Conversation

@whdgur5717
Copy link
Collaborator

@whdgur5717 whdgur5717 commented Nov 21, 2025

💡 작업 내용

  • cva,sva / styleContext API 구현

  • Tab 컴포넌트를 구현

  • Tab 컴포넌트에 대한 설명은 storybook에 작성했습니다

  • 2973466 stroke-weight에 px을 추가했습니다

  • global style의 경우에는 1. font-face선언을 제외하고, 2. reset style을 좀더 추가해야 할 필요가 있어서 추후에 더 자세하게 작업하겠습니다

💡 자세한 설명

  • 컴포넌트 스타일을 선언하고 적용할때 각자마자 스타일이 너무 다르거나, 컴포넌트 로직에 스타일이 섞이면 스타일 속성 수정 시에 너무 많은 시간이 소요되고 유지보수가 어려워진다고 생각해서 구조화된 어떤 방법이 있어야한다고 생각하는데
    emotion에서는 그런 기능을 제공하지 않지만, 반면에 build-time css 라이브러리들에서 recipe,slot 라고 불리는 유틸리티 함수들을 제공하는데
    ex. vanila-extract의 recipe , pandacss의 recipe/slot-recipe, tailwind-variants

소스코드를 참고해서 API를 구현해서 Tab을 제작했습니다

  • 좀더 확인이 필요하긴 해서 Tab 폴더 하위에만 넣어놨습니다

가장 많이 참고한 건 pandacss의 slot recipe인데
https://panda-css.com/docs/docs/concepts/slot-recipes
구현 코드도 유사하고, 설명이 자세하게 되어있어서 해당 내용 참고해보시면 됩니다

import { sva } from '../styled-system/css'
 
const tabStyles = sva({
  slots: ['root', 'control', 'label'],
  base: {
    root: { display: 'flex', alignItems: 'center', gap: '2' },
    control: { borderWidth: '1px', borderRadius: 'sm' },
    label: { marginStart: '2' }
  },
  variants: {
    size: {
      sm: {
        control: { width: '8', height: '8' },
        label: { fontSize: 'sm' }
      },
      md: {
        control: { width: '10', height: '10' },
        label: { fontSize: 'md' }
      }
    }
  },
  defaultVariants: {
    size: 'sm'
  }
})

<TabPrimitive.Trigger
        ref={ref}
        css={[
          tabStyles.base.root,
          tabStyles.variants.size[variant].control,
          tabStyles.variants.size[layout].label,
        ]}
        {...props}
      />

📗 참고 자료 (선택)

📢 리뷰 요구 사항 (선택)

✅ 셀프 체크리스트

  • 머지할 브랜치 확인했나요?
  • 이슈는 close 했나요?
  • Reviewers, Labels, Projects를 등록했나요?
  • 기능이 잘 동작하나요?
  • 불필요한 코드는 제거했나요?

closes #이슈번호

@whdgur5717 whdgur5717 linked an issue Nov 21, 2025 that may be closed by this pull request
@vercel
Copy link

vercel bot commented Nov 21, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
ject-official-web-site-client Ready Ready Preview Comment Nov 24, 2025 11:05am

- 추후 변경 예정
Copy link
Contributor

@WonJuneKim WonJuneKim left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수고 정말 많으셨습니다! 🚀

컴포넌트 스타일을 선언하고 적용할때 각자마자 스타일이 너무 다르거나, 컴포넌트 로직에 스타일이 섞이면 스타일 속성 수정 시에 너무 많은 시간이 소요되고 유지보수가 어려워진다고 생각해서 구조화된 어떤 방법이 있어야한다고 생각하는데

  • 해당 부분과 관련해서 emotion으로 작성된 커밋 로그나 직접적인 비교 대상이 없는게 아쉽네요! 현재 팀 컨벤션 상 사용하고 있는 방식에 스타일링 방식은 어느정도 맞춰져 있으나, 컴포넌트 로직 - 스타일 로직 간에 명확한 구분이 없다는 부분이 저도 아쉽긴하네요. styled 방식을 복합적인 컴포넌트의 경우 스타일링만을 위한 props가 drilling이 발생하는 side effect 도 있구요. 다만, 현재 코드에서 문제라고 생각하신 부분이나 컴포넌트 자체에 스타일링 방식에 대한 직접적인 비교가 제시되어야 팀 적인 차원에서 더욱 공감을 할 수 있을 거라고 생각됩니다!

  • 제안 해주신 스타일링 방법에 대해서 꼼꼼히 읽어보느라 리뷰가 좀 늦었네요! 제안 주신 스타일링 방식은 이전에 동영님이 계실 때 같이 한번 언급 주신 스타일링 선언 방식과도 매우 유사한 것으로 보입니다. (제 기억이 맞다면!)

  • 현재 구조는 Radix - SVA - styledContext - Emotion 의 구조를 가지게 되는데, 결국에 이러한 방식을 채택을 한다면 build-time css 를 사용하는 것이 더 적절한 방식이라고 생각이 듭니다. (styledContext가 가지고 있는 책임을 줄이고, 런타임까지 동시에 고려하게 되는 부분이 줄어들 것으로 예상되네요.

  • 또한 저의 경우 panda CSS의 sva 형태가 아닌 순수 vanilla-extract 까지 사용을 해봤는데, 확실히 드라마틱한 변경점이라 시간 소요가 많이되었네요. 팀적으로도 sva 형태의 컨벤션이나 구조를 익히는데에 꽤 시간이 소요될 것으로 판단됩니다. 작업 속도나 편의성을 위해서는 이후 디자인 시스템 개선 과정 때 피그마 내부 디자인 요소 표기 시에 이러한 구조로 보여지게 할 수 있으면 좋겠다는 생각도 드네요~

관련 코멘트 한번 확인 부탁드리고 확인하셨으면 멘션 한 번 주세요! 다시 한번 수고 많으셨습니다 :)

};

export type VariantProps<V extends VariantsConfigBase> = {
[K in keyof V]?: keyof V[K] & string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

keyof V[K]에서 자동 추론된 타입 string | number | symbol 중에 string만 골라내는 흐름이군요!

Comment on lines 232 to 241
export type RecipeVariants<R> = VariantPropsOf<R>;
export type RecipeVariantProps<R> = VariantPropsOf<R>;

// Extract slot union type from a recipe's raw() return type
export type RecipeSlots<R> = R extends { raw: (props?: any) => Record<infer S extends string, any> }
? S
: never;

// Extract the raw style map type from a recipe
export type RecipeStyles<R> = R extends { raw: (props?: any) => infer M } ? M : never;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요 부분은 아직 사용되는 구간이 없고, vanilla-extract 등이 build-time css 를 사용 시 활용할 수 있는 부분으로 보여지는데, 제가 이해한 바가 맞는지 질문드립니다~

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

타입 관련해서 정확하게 추론이 안되는 이슈가 좀 있어서 일단 만들어놨는데, 추후에는 하나의 유틸리티 타입만 남겨놓을(export할) 생각입니다

Comment on lines 236 to 238
export type RecipeSlots<R> = R extends { raw: (props?: any) => Record<infer S extends string, any> }
? S
: never;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PandaCSS의 경우 실용주의적 관점에서 any 를 사용하더라구요.
생각하기에 따라서 파라미터의 경우 any로 열어두고, infer로 추론한 타입(현재 반환 타입)에 대해서는 unknown으로 관리할 수도 있을 거 같은데, 이 부분은 어떻게 생각하시는지 궁금합니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사실 저도 임시로 하느라 any를 좀 과하게 사용한 감이있는데, 아마 정식?으로 사용된다면 최대한 any를 줄여갈 생각입니다

// TODO: 추후 공통화하기
const interactiveSelector =
'&:not(:is(:disabled, [disabled], [data-disabled], [aria-disabled=true]))';
const disabledSelector = '&:is(:disabled, [disabled], [data-disabled], [aria-disabled=true])';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

data-attribute 로 처리하는 방식이 매우 효율적인 거 같습니다.
#266 에서도 사용했었는데, 이렇게 처리하지 않으면 drilling이 발생하거나, Compound를 해야하는 경우가 많더라구요!

compoundVariants?: Array<{
variants: Partial<VariantProps<V>>;
style: SlotStyles<S>;
}>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

전형적인 headless 방식이네요.
요 부분이 가장 러닝 커브가 발생할 것으로 예상되는 부분입니다. 예를 들어 tab.styles.ts에서 작성하신 content에(72line) 빈 객체가 들어가는 방식, 구조에 맞춰 작성해야하는 스타일링 작성 방식 등이 있을 거 같네요.

추가로 궁금한게 있는데, 이럴 경우 base에는 공통 스타일, variant에는 분기가 되거나 다른 variant 스타일링을 작성하는게 맞는지 질문드려요~

Copy link
Collaborator Author

@whdgur5717 whdgur5717 Nov 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네 맞습니다
base에는 공통 , variants : {[variant명] : 스타일} 이런식을 작성하면 되는데
일단 base / variant로 나눠지는 이 패턴만 잘 인지하면(혹은 굳이 이런 유틸리티를 사용하지 않아도) 러닝커브 같은부분은 저는 괜찮을 것 같고, 사용 방식같은 부분에서는 ts를 통해서 편의성도 제공할 수 있을 것 같습니다

Comment on lines 59 to 61
attributes: true,
childList: true,
subtree: true,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

조금 더 엄격하게 제어를 하게된다면 attributeFilter 속성도 추가되어도 좋을 거 같습니다!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

++) 이러한 방식으로 제어를 하게 되면 추후에 throttle 등의 역할을 하는 기능이 함께 추가되면 더 좋을 거 같네요. (참고)

Comment on lines 112 to 122
isItemStretched: {
false: {
list: {
width: 'auto',
justifyContent: 'flex-start',
},
trigger: {
flex: 'initial',
},
},
true: {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

변경된 디자인 스타일링의 구조에서는 해당 속성이 boolean 형태가 아니라 string 타입으로 추론이 되는거같은데, 이러한 부분이 boolean 형태로 주어지는 요소들에 대해서 동일하게 작용하는지 궁금합니다.

그렇다면 명칭자체를 true false가 아니라 기본형, 확장형 네이밍 짓는 것이 더 직관적이라고 생각되는데 종혁님의 의견이 궁금합니다~

Copy link
Collaborator Author

@whdgur5717 whdgur5717 Nov 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

일단 저는 구현이 불가능한 경우가 아니라면 figma variant와 네이밍을 맞추는게 좋다고 생각을 해서 true/false를 그대로 사용했는데, string으로 그대로 가다보니 타입추론이나 적용하는게 좀 어렵긴 하네요.. 해결해보고 너무 복잡하다면 말씀하신 방법대로 수정하겠습니다

const el = listRef.current;
if (!el) return;

const t = window.setTimeout(updateIndicator, 0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const t = window.setTimeout(updateIndicator, 0);
const initialTimeout = window.setTimeout(updateIndicator, 0);
Suggested change
const t = window.setTimeout(updateIndicator, 0);
const timeout = window.setTimeout(updateIndicator, 0);

정도의 네이밍은 어떠신가용?

Comment on lines 75 to 77
borderBottomWidth: t.scheme.semantic.strokeWeight[2],
borderBottomStyle: 'solid',
borderBottomColor: t.color.semantic.stroke.bold,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

추정되는건 요부분인거 같은데,

Image

와 같이 스토리북에서 표기되더라구요!

Image

피그마 코멘트 주신 부분과도 연관이 있을 거 같아 확인차 노티드립니다!

Copy link
Collaborator Author

@whdgur5717 whdgur5717 Nov 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아 z-index 관련해서 적용이 안된것같네요 수정하겠습니다
indicator bottom을 -2로 내려서 해결했습니다

@@ -0,0 +1,361 @@
/* eslint-disable react-hooks/rules-of-hooks */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이슈가 되는 부분은 버전 업그레이드 이 후 진행되어도 좋을 거 같네요 😄

Copy link
Collaborator Author

@whdgur5717 whdgur5717 Nov 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네 이것도 eslint storybook을 설치해서 해결하면 될것 같습니다

@whdgur5717 whdgur5717 merged commit 759bedc into dev Nov 24, 2025
4 of 5 checks passed
@whdgur5717 whdgur5717 deleted the feat-tab-component branch November 24, 2025 11:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Tab 컴포넌트 구현

3 participants