@fe
(@Feca [Web FE]@Hanna [Web FE]@Juun [Web FE Lead]@Ssol [Web FE]@EATSTEAK [Web FE]@Jerome [Web FE Vice Lead])요즘 개발자라면 개인 블로그 하나 기본템으로 있던데import { fs } from 'zx' const commitMessagePath = process.argv[2] if (commitMessagePath) { const commitMessage = fs.readFileSync(commitMessagePath, 'utf-8').trim() // <https://gitmoji.dev/> const newCommitMessage = commitMessage // 🐛 Fix a bug. .replace(/^b /, ':https://a.slack-edge.com/production-standard-emoji-assets/14.0/apple-large/1f41b@2x.png|:bug:: ') // 🎨 Improve Structure / format of the codes. .replace(/^a /, ':https://a.slack-edge.com/production-standard-emoji-assets/14.0/apple-large/1f3a8@2x.png|:art:: ') // 🔥 Remove code or files. .replace(/^f /, ':https://a.slack-edge.com/production-standard-emoji-assets/14.0/apple-large/1f525@2x.png|:fire:: ') // ✨ Introduce new features. .replace(/^s /, ':https://a.slack-edge.com/production-standard-emoji-assets/14.0/apple-large/2728@2x.png|:sparkles:: ') // 💄 Update UI and style files. .replace(/^l /, ':https://a.slack-edge.com/production-standard-emoji-assets/14.0/apple-large/1f484@2x.png|:lipstick:: ') // ♻️ Refactor code. .replace(/^r /, ':https://a.slack-edge.com/production-standard-emoji-assets/14.0/apple-large/267b-fe0f@2x.png|:recycle:: ') // 💩 Write bad code that needs to be improved. .replace(/^pp /, ':poop: ') // 📦 Update compiled files or packages. .replace(/^p /, ':https://a.slack-edge.com/production-standard-emoji-assets/14.0/apple-large/1f4e6@2x.png|:package:: ') // 🔧 Write configuration file. .replace(/^w /, ':https://a.slack-edge.com/production-standard-emoji-assets/14.0/apple-large/1f527@2x.png|:wrench:: ') // 🚚 Move file or directory. .replace(/^t /, ':https://a.slack-edge.com/production-standard-emoji-assets/14.0/apple-large/1f69a@2x.png|:truck:: ') fs.writeFileSync(commitMessagePath, newCommitMessage) }
import { fs } from 'zx' const commitMessagePath = process.argv[2] if (commitMessagePath) { const commitMessage = fs.readFileSync(commitMessagePath, 'utf-8').trim() // <https://gitmoji.dev/> const newCommitMessage = commitMessage // 🐛 Fix a bug. .replace(/^b /, ':https://a.slack-edge.com/production-standard-emoji-assets/14.0/apple-large/1f41b@2x.png|:bug:: ') // 🎨 Improve Structure / format of the codes. .replace(/^a /, ':https://a.slack-edge.com/production-standard-emoji-assets/14.0/apple-large/1f3a8@2x.png|:art:: ') // 🔥 Remove code or files. .replace(/^f /, ':https://a.slack-edge.com/production-standard-emoji-assets/14.0/apple-large/1f525@2x.png|:fire:: ') // ✨ Introduce new features. .replace(/^s /, ':https://a.slack-edge.com/production-standard-emoji-assets/14.0/apple-large/2728@2x.png|:sparkles:: ') // 💄 Update UI and style files. .replace(/^l /, ':https://a.slack-edge.com/production-standard-emoji-assets/14.0/apple-large/1f484@2x.png|:lipstick:: ') // ♻️ Refactor code. .replace(/^r /, ':https://a.slack-edge.com/production-standard-emoji-assets/14.0/apple-large/267b-fe0f@2x.png|:recycle:: ') // 💩 Write bad code that needs to be improved. .replace(/^pp /, ':poop: ') // 📦 Update compiled files or packages. .replace(/^p /, ':https://a.slack-edge.com/production-standard-emoji-assets/14.0/apple-large/1f4e6@2x.png|:package:: ') // 🔧 Write configuration file. .replace(/^w /, ':https://a.slack-edge.com/production-standard-emoji-assets/14.0/apple-large/1f527@2x.png|:wrench:: ') // 🚚 Move file or directory. .replace(/^t /, ':https://a.slack-edge.com/production-standard-emoji-assets/14.0/apple-large/1f69a@2x.png|:truck:: ') fs.writeFileSync(commitMessagePath, newCommitMessage) }깃모지 풀로 작성안해도 굉장히 편하게 쓸 수 있습니다 (편집됨)
import { ComplexButton } from "complex-button"; import { Link } from "react-router"; export function ButtonAsLink() { return (<> {/* radix style */} <ComplexButton asChild> <Link to="/link"> Hello </Link> </ComplexButton> {/* `as` Style */} <ComplexButton as={Link} /> </>); }
@radix-ui/react-slot
이 참 좋아서 애용하고 있긴 한데, asChild를 쓸 경우 children까지 신경써야 하는 경우가 있어서 as
prop같은 스타일도 같이 사용할까 생각 중이에요(<= 근데 라이브러리 뭐 쓸지 못찾아서 고민).여기에 더해서 ComplexButtonGroup 같은 컴포넌트를 만든다고 할 때, query parameter를 조작할 일이 있으면(singular ButtonGroup의 selection 상태를 query parameter로 컨트롤) 당연히 Link
컴포넌트를 사용해서 시멘틱 맥락을 노출하는 것이 좋은데, 이걸 깔끔하게 할 방법이 떠오르지 않더라고요 (일반 ButtonGroup이면 onClick과 이벤트 핸들링으로 전파가 가능한데, query params를 사용하면 Item의 각 value마다 query params를 가진 url을 렌더해줘야 하니까 동작이 크게 달라짐).이걸 일반화 하려면 어떤 식으로 사용하는 게 좋을까요?제가 생각하는 to-be 는 아래랑 비슷한데, 이걸 구현하는 좋은 방법이나 아이디어 있으면 의견 부탁드려요.import { ComplexButton } from "complex-button"; import { Link } from "react-router"; export function ButtonAsLink() { return (<> {/* radix style */} <ComplexButton asChild> <Link to="/link"> Hello </Link> </ComplexButton> {/* `as` Style */} <ComplexButton as={Link} /> </>); }TL;DR
asChild
처럼 시멘틱 태그 변경할 때 어떤 방식이 좋을까Button
<-> Link
) (편집됨)node_modules
하위로 호이스팅 시키지 않는 변경이 있었는데 VSCode ESLint 플러그인 서버가 호이스팅 되지 않는 ESLint 플러그인을 찾지 못해서 발생하는 문제인 것 같습니다. (ESLint CLI 환경에서는 문제 없음) (관련 이슈)해결 방법: 프로젝트 루트 디렉토리에 .npmrc
파일을 만들고 아래 내용 추가public-hoist-pattern[]=*eslint* public-hoist-pattern[]=*prettier*(편집됨)
import { ComplexButton } from "complex-button"; import { Link } from "react-router"; export function ButtonAsLink() { return (<> {/* radix style */} <ComplexButton asChild> <Link to="/link"> Hello </Link> </ComplexButton> {/* `as` Style */} <ComplexButton as={Link} /> </>); }
@radix-ui/react-slot
이 참 좋아서 애용하고 있긴 한데, asChild를 쓸 경우 children까지 신경써야 하는 경우가 있어서 as
prop같은 스타일도 같이 사용할까 생각 중이에요(<= 근데 라이브러리 뭐 쓸지 못찾아서 고민).여기에 더해서 ComplexButtonGroup 같은 컴포넌트를 만든다고 할 때, query parameter를 조작할 일이 있으면(singular ButtonGroup의 selection 상태를 query parameter로 컨트롤) 당연히 Link
컴포넌트를 사용해서 시멘틱 맥락을 노출하는 것이 좋은데, 이걸 깔끔하게 할 방법이 떠오르지 않더라고요 (일반 ButtonGroup이면 onClick과 이벤트 핸들링으로 전파가 가능한데, query params를 사용하면 Item의 각 value마다 query params를 가진 url을 렌더해줘야 하니까 동작이 크게 달라짐).이걸 일반화 하려면 어떤 식으로 사용하는 게 좋을까요?제가 생각하는 to-be 는 아래랑 비슷한데, 이걸 구현하는 좋은 방법이나 아이디어 있으면 의견 부탁드려요.import { ComplexButton } from "complex-button"; import { Link } from "react-router"; export function ButtonAsLink() { return (<> {/* radix style */} <ComplexButton asChild> <Link to="/link"> Hello </Link> </ComplexButton> {/* `as` Style */} <ComplexButton as={Link} /> </>); }TL;DR
asChild
처럼 시멘틱 태그 변경할 때 어떤 방식이 좋을까Button
<-> Link
) (편집됨)asChild
패턴으로 위임하려고 노력해요.function ButtonWithContent({ asChild }: ComponentProps) { const Comp = asChild ? 'button' : Slot; return ( <Comp> <Icon> hello </Comp> ); } function ButtonAsLinkPage() { return ( <ButtonWithContent asChild> <Link to="hello" /> </ButtonWithContent> ); }
<Icon> Hello
가 그대로 전달되는 거죠? 사실 해보면 되긴 한데.. 이렇게 쓰는게 좋은 방법인가 궁금하기도 해서 물어봅니다polymorphic (as
prop)
polymorhpic은 단순하고 투명하다라는 점이 좋았어요.
런타임에서 해결시키는 기능들이 적고, 주로 props(i.e.As Props)로 소통 해요. 그래서 코드가 단순해지고 투명해져요. 작업자 누구나 as props가 어떤식으로 흘러가는지 쉽게 확인할 수 있는게 큰 장점이었어요.
만약 다른 프로젝트에서 도입을 해본다면 아래 내용들을 고려해볼 것 같아요.
• 컴포넌트의 껍데기에 다형적인 기능을 추가하는 것 외의 용도로 사용하려고 하면 코드관리가 어렵고, 특유의 순수성도 많이 헤치는 것 같아요. 그래서 atomic(예: header, button) 레벨의 요소에 적용해두는게 가장 적합해요.
• typescript를 사용한다면 컴포넌트 정의부에서 복잡한 타입을 정의해놔야해요. 작업자마다 타입에 대한 이해수준이 다를 수 있어서 거부감을 느낄 수 있어요. 저희팀은 MUI의 다형타입 유틸타입을 참고하여 다형타입을 정의해주는 유틸타입을 만들었어요.
Rendering Delegation (asChild
)
rendering delegation은 런타임 기능들이 많아서 컴포넌트의 조합을 유연하고 우아하게 처리할 수 있는게 좋았어요.
만약 다른 프로젝트에서 도입을 해본다면 아래 내용들을 고려해볼 것 같아요.
• 컴포넌트의 기능이 완전히 정의된 상태에서는 이 방법이 유용해요. 하지만, 만약 기능 정의가 안된 컴포넌트에 무작정 asChild를 열어두면 혼란함을 야기할 것 같아요. 코드가 어떻게 적용되는지 불투명하다보니 '기준 기능 명세'가 없으면 불안해요.
• slottable한 시스템이 꼭 필요할지? : 생각보다 컴포넌트의 기능을 정의해두고 만드는 케이스가 많지 않아요 (팀마다, 디자인시스템 정책마다 다를 수 있어요). 그렇다면 굳이 공통화된 slottable한 시스템을 구축하기보단, 위 글에서 언급해주신 방법론들을 적절히 조합해서 사용하는게 더 유리할 수 있어요.(편집됨)
[1,2,3].map(v => { const isSelected = query === v return <Link href={`/url?query=${value}`} {...뭐 여기서 이제 isSelected로 스타일 바꾸든 뭐하든} /> })
asChild
로 렌더링할 컴포넌트를 <Link/>
컴포넌트로 변경해서 해결할 수 있지 않을까요?위 댓글에서 asChild
를 통한 다형성 구현 방식의 단점으로 지적한 코드가 어떻게 적용되는지 불투명하다보니 '기준 기능 명세'가 없으면 불안해요.
를 해소하기 위해서는 asChild
다형성을 지원하는 컴포넌트를 직접 구현하는 것보다 일관적인 동작을 보장하는 Radix UI 컴포넌트를 적극적으로 사용하는 것이 예측 가능성과 디버깅 난이도 면에서 좋을 것 같아요!export function ComplexButtonGroupExample() { const [search] = useSearchParams(); const query = useMemo(() => search.get("query") ?? "1", [search]); return ( <ComplexButtonGroup> <Button asChild value="1"> <Link to="/url?value=1"/> <Button/> <Button asChild value="2"> <Link to="/url?value=2"/> <Button/> <Button asChild value="3"> <Link to="/url?value=3"/> <Button/> </ComplexButtonGroup> ); }(편집됨)
외부(searchParams)에 위임한거니 그룹 컴포넌트가 자식의 상태 변경 여부를 알면 안되겠다 싶었음.ㄹㅇㅋㅋ
import { ComplexButton } from "complex-button"; import { Link } from "react-router"; export function ButtonAsLink() { return (<> {/* radix style */} <ComplexButton asChild> <Link to="/link"> Hello </Link> </ComplexButton> {/* `as` Style */} <ComplexButton as={Link} /> </>); }
@radix-ui/react-slot
이 참 좋아서 애용하고 있긴 한데, asChild를 쓸 경우 children까지 신경써야 하는 경우가 있어서 as
prop같은 스타일도 같이 사용할까 생각 중이에요(<= 근데 라이브러리 뭐 쓸지 못찾아서 고민).여기에 더해서 ComplexButtonGroup 같은 컴포넌트를 만든다고 할 때, query parameter를 조작할 일이 있으면(singular ButtonGroup의 selection 상태를 query parameter로 컨트롤) 당연히 Link
컴포넌트를 사용해서 시멘틱 맥락을 노출하는 것이 좋은데, 이걸 깔끔하게 할 방법이 떠오르지 않더라고요 (일반 ButtonGroup이면 onClick과 이벤트 핸들링으로 전파가 가능한데, query params를 사용하면 Item의 각 value마다 query params를 가진 url을 렌더해줘야 하니까 동작이 크게 달라짐).이걸 일반화 하려면 어떤 식으로 사용하는 게 좋을까요?제가 생각하는 to-be 는 아래랑 비슷한데, 이걸 구현하는 좋은 방법이나 아이디어 있으면 의견 부탁드려요.import { ComplexButton } from "complex-button"; import { Link } from "react-router"; export function ButtonAsLink() { return (<> {/* radix style */} <ComplexButton asChild> <Link to="/link"> Hello </Link> </ComplexButton> {/* `as` Style */} <ComplexButton as={Link} /> </>); }TL;DR
asChild
처럼 시멘틱 태그 변경할 때 어떤 방식이 좋을까Button
<-> Link
)=> 그래서 굳이 Link 쓸 필요없이 그냥 상태 변경 액션처럼 처리해서 쿼리스트링 동기화 시켜버림결국 이걸 채택 안한 가장 큰 이유는 사용자가 query params를 변경하는 액션을 할 때 programmatic routing이 아닌 link navigation을 통해 하도록 하기 위해서였음예) Pagination 하는데 Ctrl+클릭으로 다음 페이지를 새 탭으로 열 수 없다면?
ex) 클릭하면 드롭다운 나오고 선택으로 필터링 가능한 목록 나오고, 선택하면 쿼리스트링 바뀌면서 컨텐츠 바뀌는거위 일련의 과정속에서 사용자에게 굳이 하이퍼링크라고 표시해줄 이유는 없다고 생각함.
최종 사용자 동작 측면으로 봤을 때에는 전혀 단점이 없는 구현고건 ㅇㅈ
Error / 값이 비어 있어요.: Error: 값이 비어 있어요. at assertNonNullish (/home/feca_urssu/mention-bot/src/utils/assertion.ts:16:27) at parseAttachment (/home/feca_urssu/mention-bot/src/utils/archive.ts:270:3) at Array.map (<anonymous>) at transformToPreArchivedMessage (/home/feca_urssu/mention-bot/src/utils/archive.ts:359:31) at processTicksAndRejections (node:internal/process/task_queues:95:5) at async Promise.all (index 26) at handleArchiveMessage (/home/feca_urssu/mention-bot/src/events/message.ts:195:33) at Array.<anonymous> (/home/feca_urssu/mention-bot/node_modules/.pnpm/@slack+bolt@4.2.1_@types+express@5.0.0/node_modules/@slack/bolt/src/middleware/builtin.ts:238:5) at Array.<anonymous> (/home/feca_urssu/mention-bot/node_modules/.pnpm/@slack+bolt@4.2.1_@types+express@5.0.0/node_modules/@slack/bolt/src/middleware/builtin.ts:288:5) at Array.onlyEvents (/home/feca_urssu/mention-bot/node_modules/.pnpm/@slack+bolt@4.2.1_@types+express@5.0.0/node_modules/@slack/bolt/src/middleware/builtin.ts:108:5)(편집됨)
import { ComplexButton } from "complex-button"; import { Link } from "react-router"; export function ButtonAsLink() { return (<> {/* radix style */} <ComplexButton asChild> <Link to="/link"> Hello </Link> </ComplexButton> {/* `as` Style */} <ComplexButton as={Link} /> </>); }
@radix-ui/react-slot
이 참 좋아서 애용하고 있긴 한데, asChild를 쓸 경우 children까지 신경써야 하는 경우가 있어서 as
prop같은 스타일도 같이 사용할까 생각 중이에요(<= 근데 라이브러리 뭐 쓸지 못찾아서 고민).여기에 더해서 ComplexButtonGroup 같은 컴포넌트를 만든다고 할 때, query parameter를 조작할 일이 있으면(singular ButtonGroup의 selection 상태를 query parameter로 컨트롤) 당연히 Link
컴포넌트를 사용해서 시멘틱 맥락을 노출하는 것이 좋은데, 이걸 깔끔하게 할 방법이 떠오르지 않더라고요 (일반 ButtonGroup이면 onClick과 이벤트 핸들링으로 전파가 가능한데, query params를 사용하면 Item의 각 value마다 query params를 가진 url을 렌더해줘야 하니까 동작이 크게 달라짐).이걸 일반화 하려면 어떤 식으로 사용하는 게 좋을까요?제가 생각하는 to-be 는 아래랑 비슷한데, 이걸 구현하는 좋은 방법이나 아이디어 있으면 의견 부탁드려요.import { ComplexButton } from "complex-button"; import { Link } from "react-router"; export function ButtonAsLink() { return (<> {/* radix style */} <ComplexButton asChild> <Link to="/link"> Hello </Link> </ComplexButton> {/* `as` Style */} <ComplexButton as={Link} /> </>); }TL;DR
asChild
처럼 시멘틱 태그 변경할 때 어떤 방식이 좋을까Button
<-> Link
)