
π― μ»΄ν¬λνΈλ₯Ό λ§λ€κ³ ν μ€νΈ μ½λλ₯Ό ν΅ν΄ κ²μ¦ν©λλ€.
// π Title.tsx
import { styled } from 'styled-components';
import { ColorKey, HeadingSize, Theme } from '../../style/theme';
interface Props {
children: React.ReactNode;
size: HeadingSize;
color?: ColorKey;
}
export default function Title({ children, size }: Props) {
return <TitleStyle size={size}>{children}</TitleStyle>;
}
const TitleStyle = styled.h1<Omit<Props, 'children'>>`
font-size: ${({ theme, size }) => (theme as Theme).heading[size].fontSize};
color: ${({ theme, color }) =>
color ? (theme as Theme).color[color] : (theme as Theme).color.primary};
`;
Titleμ <h1> νκ·Έλ‘ λ λλλ©°, styledλ‘ μ€νμΌμ΄ μ§μ λ©λλ€.
font-size : μ λ¬λ°μ size ν€μ λ°λΌ ν
λ§μμ ν΄λΉ ν°νΈ μ¬μ΄μ¦ κ°μ κ°μ Έμ΅λλ€.
color : colorκ° μλ€λ©΄ ν΄λΉ μμμ μ¬μ©νκ³ , μμΌλ©΄ primary μμμ μ¬μ©ν©λλ€.
ν
μ€νΈλ @testing-library/reactλ₯Ό μ¬μ©νμ¬ λ λλ§ κ²°κ³Όλ₯Ό νμΈν©λλ€.
// π Title.spec.tsx
import { render, screen } from '@testing-library/react';
import Title from './Title';
import { BookStoreThemeProvider } from '../../context/ThemeContext';
describe('Title μ»΄ν¬λνΈ ν
μ€νΈ', () => {
it('λ λλ§ νμΈ', () => {
render(
<BookStoreThemeProvider>
<Title size='large'>μ λͺ©</Title>
</BookStoreThemeProvider>
);
expect(screen.getByText('μ λͺ©')).toBeInTheDocument();
});
it('size props μ μ©', () => {
const { container } = render(
<BookStoreThemeProvider>
<Title size='large'>μ λͺ©</Title>
</BookStoreThemeProvider>
);
expect(container?.firstChild).toHaveStyle({ fontSize: '2rem' });
});
it('color props μ μ©', () => {
const { container } = render(
<BookStoreThemeProvider>
<Title size='large' color='primary'>
μ λͺ©
</Title>
</BookStoreThemeProvider>
);
expect(container?.firstChild).toHaveStyle({ color: 'brown' });
});
});
describe : ν
μ€νΈν κ·Έλ£Ήμ λ¬Άλ μ©λμ
λλ€.
[ ν μ€νΈ 1 ] 'λ λλ§ νμΈ'
render(...): Title μ»΄ν¬λνΈλ₯Ό νλ©΄μ λ λλ§ μν΅λλ€.
<BookStoreThemeProvider> : styled-componentsμμ ν
λ§ κ°μ μ°κΈ° μν΄ κ°μΈμ€λλ€.
screen.getByText('μ λͺ©') : λ λλ νλ©΄μμ 'μ λͺ©'μ΄λΌλ ν
μ€νΈλ₯Ό κ°μ§ μμλ₯Ό μ°Ύμ΅λλ€.
expect(...).toBeInTheDocument() : μ°Ύμ μμκ° νλ©΄μ μ‘΄μ¬νλμ§λ₯Ό νμΈν©λλ€.
[ ν μ€νΈ 2 ] 'μ¬μ΄μ¦ νμΈ'
render(...) : size='large'λ₯Ό λ£μ Title μ»΄ν¬λνΈλ₯Ό νλ©΄μ λ λλ§ μν΅λλ€.
container.firstChild : λ λλ μ»΄ν¬λνΈμ κ°μ₯ λ°κΉ₯ μμ (<h1>)λ₯Ό κ°μ Έμ΅λλ€.
expect(...).toHaveStyle({ fontSize: '2rem' }) : μ°Ύμ μμκ° font-size: 2rem μ€νμΌμ κ°μ§κ³ μλμ§ νμΈν©λλ€.
[ ν μ€νΈ 3 ] 'μκΉ νμΈ'
render(...) : color='primaryλ₯Ό λ£μ Title μ»΄ν¬λνΈλ₯Ό νλ©΄μ λ λλ§ μν΅λλ€.
container.firstChild : λ λλ μ»΄ν¬λνΈμ κ°μ₯ λ°κΉ₯ μμ (<h1>)λ₯Ό κ°μ Έμ΅λλ€.
expect(...).toHaveStyle({ color: 'brown' }) : μ°Ύμ μμκ° color.primary = 'brown' μ€νμΌμ κ°μ§κ³ μλμ§ νμΈν©λλ€.
// π Button.tsx
import { styled } from 'styled-components';
import { ButtonScheme, ButtonSize, Theme } from '../../style/theme';
interface Props {
children: React.ReactNode;
size: ButtonSize;
scheme: ButtonScheme;
disabled?: boolean;
isLoading?: boolean;
}
export default function Button({
children,
size,
scheme,
disabled,
isLoading,
}: Props) {
return (
<ButtonStyle
size={size}
scheme={scheme}
disabled={disabled}
isLoading={isLoading}
>
{children}
</ButtonStyle>
);
}
const ButtonStyle = styled.button<Omit<Props, 'children'>>`
font-size: ${({ theme, size }) => (theme as Theme).button[size].fontSize};
padding: ${({ theme, size }) => (theme as Theme).button[size].padding};
color: ${({ theme, scheme }) => (theme as Theme).buttonScheme[scheme].color};
background-color: ${({ theme, scheme }) =>
(theme as Theme).buttonScheme[scheme].backgroundColor};
border: 0;
border-radius: ${({ theme }) => (theme as Theme).borderRadius.default};
opacity: ${({ disabled }) => (disabled ? 0.5 : 1)};
pointer-events: ${({ disabled }) => (disabled ? 'none' : 'auto')};
cursor: ${({ disabled }) => (disabled ? 'none' : 'pointer')};
`;
ButtonStyleμ <button> νκ·Έλ‘ λ λλλ©°, propsλ₯Ό λ°μ μ€νμΌμ μ
νμ€λλ€.
theme.button[size] β ν¬κΈ°λ³ font-size, paddingμ μ§μ ν©λλ€.
theme.buttonScheme[scheme] β μμ μ€νμΌλ³ color, backgroundColorλ₯Ό μ§μ ν©λλ€.
disabled β opacity, pointer-events, cursor μμ±μ μ§μ ν©λλ€.
// π Button.spec.tsx
import { render, screen } from '@testing-library/react';
import Button from './Button';
import { BookStoreThemeProvider } from '../../context/ThemeContext';
describe('Button μ»΄ν¬λνΈ ν
μ€νΈ', () => {
it('λ λλ§ νμΈ', () => {
render(
<BookStoreThemeProvider>
<Button size='large' scheme='primary'>
λ²νΌ
</Button>
</BookStoreThemeProvider>
);
expect(screen.getByText('λ²νΌ')).toBeInTheDocument();
});
it('κΈμ ν¬κΈ° νμΈ', () => {
render(
<BookStoreThemeProvider>
<Button size='large' scheme='primary'>
λ²νΌ
</Button>
</BookStoreThemeProvider>
);
expect(screen.getByRole('button')).toHaveStyle({ fontSize: '1.5rem' });
});
it('μμ νμΈ', () => {
render(
<BookStoreThemeProvider>
<Button size='large' scheme='primary'>
λ²νΌ
</Button>
</BookStoreThemeProvider>
);
expect(screen.getByRole('button')).toHaveStyle({ color: 'white' });
});
});
[ ν μ€νΈ 1 ] 'λ λλ§ νμΈ'
screen.getByText('λ²νΌ') : λ λλ νλ©΄μμ 'λ²νΌ'μ΄λΌλ ν
μ€νΈλ₯Ό κ°μ§ μμλ₯Ό μ°Ύμ΅λλ€.
expect(...).toBeInTheDocument() : μ°Ύμ μμκ° νλ©΄μ μ‘΄μ¬νλμ§λ₯Ό νμΈν©λλ€.
[ ν μ€νΈ 2 ] 'κΈμ ν¬κΈ° νμΈ'
screen.getByRole('button') :
expect(...).toHaveStyle({ fontSize: '1.5rem' }): μ°Ύμ μμκ° font-size: 1.5rem μ€νμΌμ κ°μ§κ³ μλμ§ νμΈν©λλ€.
[ ν μ€νΈ 3 ] 'μμ νμΈ'
- screen.getByRole('button') :
expect(...).toHaveStyle({ color: 'white' }): μ°Ύμ μμκ° color: 'white' μ€νμΌμ κ°μ§κ³ μλμ§ νμΈν©λλ€.// π InputText.tsx
import React, { ForwardedRef } from 'react';
import styled from 'styled-components';
import { Theme } from '../../style/theme';
interface Props {
placeholder?: string;
}
const InputText = React.forwardRef(
({ placeholder }: Props, ref: ForwardedRef<HTMLInputElement>) => {
return <InputTextStyle placeholder={placeholder} ref={ref} />;
}
);
const InputTextStyle = styled.input.attrs({ type: 'text' })`
padding: 0.25rem 0.75rem;
border: 1px solid ${({ theme }) => (theme as Theme).color.border};
border-radius: ${({ theme }) => (theme as Theme).borderRadius.default};
font-size: 1rem;
line-height: 1.5;
color: ${({ theme }) => (theme as Theme).color.text};
`;
export default InputText;
InputTextμ forwardRefλ₯Ό μ΄μ©ν΄ μΈλΆ μ»΄ν¬λνΈμμ μ λ¬λ°μ refλ₯Ό <input> νκ·Έμ μ°κ²°ν΄μ£Όλ μν μ ν©λλ€.
styled.input.attrs({ type: 'text' }) : type: 'text'μΈ input μμλ₯Ό λ λλ§νκ² λ©λλ€.
π€ μ
forwardRefκ° νμν κ±ΈκΉ?κΈ°λ³Έμ μΌλ‘ Reactλ ν¨μν μ»΄ν¬λνΈμ
refλ₯Ό μ§μ μ λ¬νμ§ λͺ»ν©λλ€.
refλ μΌλ°propsμ λ€λ₯΄κ² μ²λ¦¬λκΈ° λλ¬Έμ, μμ μ»΄ν¬λνΈμ λκΈ°κ³ μΆλ€λ©΄,React.forwardRefλ‘ κ°μΈμ€μΌν©λλ€.// λΆλͺ¨ function Parent() { const inputRef = useRef(null); return <ChildInput ref={inputRef} />; } // μμ const ChildInput = React.forwardRef((props, ref) => { return <input ref={ref} />; });
// π InputText.spec.tsx
import { render, screen } from '@testing-library/react';
import InputText from './InputText';
import { BookStoreThemeProvider } from '../../context/ThemeContext';
import React from 'react';
describe('InputText μ»΄ν¬λνΈ ν
μ€νΈ', () => {
it('λ λλ§ νμΈ', () => {
render(
<BookStoreThemeProvider>
<InputText placeholder='μ¬κΈ°μ μ
λ ₯νμΈμ' />
</BookStoreThemeProvider>
);
expect(
screen.getByPlaceholderText('μ¬κΈ°μ μ
λ ₯νμΈμ')
).toBeInTheDocument();
});
it('forwardRef ν
μ€νΈ', () => {
const ref = React.createRef<HTMLInputElement>();
render(
<BookStoreThemeProvider>
<InputText placeholder='μ¬κΈ°μ μ
λ ₯νμΈμ' ref={ref} />
</BookStoreThemeProvider>
);
expect(ref.current).toBeInstanceOf(HTMLInputElement);
});
});
[ ν μ€νΈ 1 ] 'λ λλ§ νμΈ'
screen.getByPlaceholderText('μ¬κΈ°μ μ
λ ₯νμΈμ') : placeholder μμ±μ κΈ°μ€μΌλ‘ μΈν μμλ₯Ό μ°Ύμ λ°νν©λλ€.
expect(...).toBeInTheDocument() : μ°Ύμ μμκ° νλ©΄μ μ‘΄μ¬νλμ§λ₯Ό νμΈν©λλ€.
[ ν μ€νΈ 2 ] 'forwardRef ν μ€νΈ'
ref.current : forwardRefλ₯Ό ν΅ν΄ μ λ¬λ ref κ°μ²΄λ‘, νμ¬ μ°κ²°λ DOM μμλ₯Ό μ°Έμ‘°ν©λλ€.
expect(...).toBeInstanceOf(HTMLInputElement) : μ°Ύμ μμκ° <input> μμμΈμ§ νμΈν©λλ€.
ν
μ€νΈ μ½λ μ체λ₯Ό μ§λ κ²μλ μ΄λ €μμ΄ μμμ§λ§, styled-componentsμ νμ
μ λν΄ μ΅μμΉ μλ€λ³΄λ μ΄λ €μ λ κ±° κ°λ€.