lit-element에서 스토어를 사용하는 경우 스토리북에서 스토어를 mocking하는 방법입니다.
storybook을 쓰다가 특정 상황에 컴포넌트가 어떻게 보여지는지 작성하고 싶을 때가 있습니다. 보통은 args 파라미터값을 줘서 element의 attribute를 준다던가 slot에 들어갈 값을 줘서 해결할 수 있습니다.
// FilledCard.stories.ts
// 불필요한 영역은 생략
...
type Args = {
content: string | TemplateResult<1>;
};
/**
* 아래는 slot을 이용하는 lit-element의 Template 코드,
* 아래 방식이 아니어도 attribute를 주거나 외부를 감싸는
* element를 별도로 작성해 실제 앱과 같은 환경을 구성할 수 있다.
*/
const Template: Story<Args> = (args: Args) =>
html`<filled-card>${args.content}</filled-card>`;
export const Empty = Template.bind({});
Empty.args = {
content: "",
};
export const Text = Template.bind({});
Text.args = {
content: "Lorem Ipsum",
};
export const Image = Template.bind({});
Image.args = {
content: html`<img src="${noImageUrl}" />`,
};
하지만 스토어에서 값을 읽는 경우 스토리북에서 args를 통해 조절할 수 없습니다. 이 경우 스토어를 mocking해서 스토어가 특정 데이터를 갖고 있는 경우를 재현할 수 있습니다.
예를 들어 아래처럼 Channel 클래스에서 copyList 속성값을 읽는 경우입니다.
// CopyList.ts
import { LitElement, css, html } from "lit";
import { customElement, state } from "lit/decorators.js";
/**
* Channel 클래스를 import, 여기선 Channel 클래스를 스토어처럼 사용하지만 본인의 개발환경에 맞게 import하면 됩니다.
예를 들면 pinia를 쓰는 경우
import { useCopyStore } from "@/store/copy"
*/
import Channel from "@/classes/Channel";
@customElement("copy-list")
export default class CopyList extends LitElement {
@state()
copyList: Copy[] = [];
constructor() {
super();
this.#created();
}
render() {
return html`
<ul class="copy-list">
${this.copyList.map(
(copy) =>
html` <li>
<filled-card class="card">
<div>
${copy}
</div>
</filled-card>
</li>`
)}
</ul>
`;
}
async #created() {
this.copyChannel = new Channel();
this.copyList = [...this.copyChannel.copyList];
}
}
위 컴포넌트는 copyChannel
로부터 읽은 copyList 배열을 통해 여러 <li>
요소를 랜더링하고 있습니다.
// CopyList.stories.ts
import { Story, Meta } from "@storybook/web-components";
import { html } from "lit-html";
/**
* CopyList 컴포넌트 등록
*/
import "@/components/CopyList";
export default {
title: "CopyList",
} as Meta;
type Args = {
parameters: { design: Record<string, string>; store: Record<string, object> };
};
const Template: Story<Args> = () =>
html`<copy-list></copy-list>`;
/**
* parameters에 data를 작성한다.
*/
export const Default = Template.bind({});
Default.parameters = {
data: {
copyList:["a","b","c"],
},
};
먼저 root에 __mocks__
파일을 만들어줍니다.
// __mocks__/classes/Channel.ts
let data = [];
export default class Channel {
copyList = [];
constructor() {
this.copyList = data;
}
}
export function decorator(story, { parameters }) {
if (parameters && parameters.data) {
data = parameters.data;
}
return story();
}
Channel.copyList
가 반환할 decorator를 만들어줍니다. 이 데코레이터는 parameters.data
를 받아 Channel.copyList
가 반환할 값을 설정해줍니다. 즉
export const Default = Template.bind({});
Default.parameters = {
data: {
copyList:["a","b","c"],
},
};
스토리에서 parameter로 설정한 값을 받아 저장하고 Channel에 접근시 이 데이터를 반환하게 할겁니다.
.storybook/main.cjs
파일에서 webpackFinal
혹은 viteFinal
속성을 수정합니다.
// .storybook/main.cjs
const { mergeConfig } = require("vite");
const { resolve } = require("path");
module.exports = {
... // 불필요한 코드 생략
async viteFinal(config) {
return mergeConfig(config, {
resolve: {
alias: {
"@/classes/Channel": require.resolve(
"../__mocks__/classes/Channel.ts"
),
"@mocks": resolve(__dirname, "../__mocks__"),
"@": resolve(__dirname, "../src"),
},
},
});
},
};
이렇게 되면 @/classes/Channel
경로를 import 하는 코드가 스토리북에선 ../__mocks__/classes/Channel.ts
를 import하도록 동작하게 됩니다.
// .storybook/preview.cjs
import { html } from "lit-html";
import { decorator as channelDecorator } from "/__mocks__/classes/Channel.ts";
export const decorators = [
channelDecorator,
];
CopyList.stories.ts
파일에 parameters.data를 적용하면
스토리북에서 이렇게 볼 수 있습니다.