기본 컴포넌트 작성
Title 컴포넌트
스니펫으로 기본 리액트 컴포넌트 작성
➡️ rfce (ES7+ React/Redux/React-Native snippets 활용)
참고 : https://github.com/r5n-labs/vscode-react-javascript-snippets/blob/HEAD/docs/Snippets.md
vscode-react-javascript-snippets/docs/Snippets.md at 185bb91a0b692c54136663464e8225872c434637 · r5n-labs/vscode-react-javascrip
Extension for React/Javascript snippets with search supporting ES7+ and babel features - r5n-labs/vscode-react-javascript-snippets
github.com
Props 타입 지정
- 항상 컴포넌트 만들 때 props 타입 지정해주는 습관 들일 것
- 여기서 필요한 속성은 children과 size
- children은 React.ReactNode로 지정해주고, size는 theme.ts에서 만든 HeadingSize를 import해온다.
HeadingSize 만들기
theme.ts
// 타이틀 사이즈 타입
export type HeadingSize = "large" | "medium" | "small";
export interface Theme {
name: ThemeName;
color: Record<ColorKey, string>;
heading?: {
[key in HeadingSize] : {
fontSize: string;
fontWeight: number;
}
}
}
export const light: Theme = {
name: 'light',
color: {
primary: 'brown',
background: 'lightgray',
secondary: 'blue',
third: 'green',
},
heading: {
large: {
fontSize: '2rem',
fontWeight: 700,
},
medium: {
fontSize: '1.5rem',
fontWeight: 700,
},
small: {
fontSize: '1rem',
fontWeight: 700,
},
}
};
export const dark: Theme = {
...light, // 중복 작성 막기 위해 얕은 복사 후 오버라이딩
name: 'dark',
color: {
primary: 'coral',
background: 'midnightblue',
secondary: 'darkblue',
third: 'darkgreen',
},
};
- HeadingSize에 대한 타입을 지정해주고 export
- 각 타입에 대한 fontSize랑 fontWeight 지정
- light 테마에 적은 내용 dark 테마에 ...연산자로 얕은 복사
TitleStyle 설정 - 테마에서 가져오기
- *Omit<> 타입으로 Props에서 children 프로퍼티 제거
- font-size : theme.heading[size]로 가져오기
- color : 타입을 옵셔널로 지정해주고, props로 color 받는다. color 없을 경우 primary 적용
interface Props {
children: React.ReactNode;
size: HeadingSize,
color?: ColorKey;
}
function Title({children, size, color}: Props){
return <TitleStyle size={size} color={color}>{children}</TitleStyle>
}
// 여기서 {theme}은 themeProvider에서 props로 넘겨주고 있는 값
const TitleStyle = styled.h1<Omit<Props, 'children'>>`
font-size: ${({theme, size}) => theme.heading[size].fontSize};
color: ${({theme, color}) => color ? theme.color[color] : theme.color.primary};
`;
※ Omit<> : 유틸리티(제너릭) 타입의 일종. 특정 속성만 제거한 타입을 정의
Omit<{타입 객체}, {제거할 프로퍼티}>
참고 : https://kyounghwan01.github.io/blog/TS/fundamentals/utility-types/#partial
Title 적용하기
Home.tsx
function Home(){
return (
<>
<Title size="medium" color="background">
제목 테스트
</Title>
<div>home body</div>
</>
)
}
Title 컴포넌트 테스트 작성하기
Title.spec.tsx
import { render, screen } from "@testing-library/react";
import Title from "./Title";
import { BookStoreThemeProvider } from "../../context/ThemeContext";
describe("Title 컴포넌트 테스트", () => {
it('렌더를 확인', () => {
// 1. 렌더 : 가상화면 렌더됨
// 적적한 props 넣어줘야 함
render(
<BookStoreThemeProvider>
<Title size="large">제목</Title>
</BookStoreThemeProvider>
);
// 2. 확인 : '제목'이란 텍스트 화면상에 있는지 확인
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"
});
})
})
- @testing-library/react 모듈에서 render와 screen 가져오기
- describe 안에 테스트할 목록 작성
- it('테스트할 상세 목록', 콜백함수)
- 콜백함수 안에 1. 렌더 2. 확인 작성
- 렌더 : 가상화면. 실제와 같은 형식 갖춰서 넣어줘야 함
- 확인 : expect(screen.getByText("제목")).toBeInTheDocument();
➡️ '제목'이란 텍스트 화면상에 있는지 확인
- 렌더 후 그 안에 props를 테스트 하고 싶으면 render 내용을 container에 담는다.
- expect 시 toHaveStyle 사용
- 테스트 실행 : npm run test Title
Button 컴포넌트
Button.tsx 만들기
- 스니펫 작성
- 자주 컴포넌트를 만들 것 같아서 스니펫을 적용했다.
- styled-components 적용 버전
- ctrl + shift + p > snippet 검색 > configure snippets 선택 > typescriptreact.json 선택
- typescriptreact.json에 넣어줘야 함. (.tsx에서 스니펫 적용 시)
- typescript.json은 ts일 경우
{
"Styled Components Component": {
"prefix": "stycomp",
"body": [
"import styled from \"styled-components\";",
"",
"interface Props {",
"",
"}",
"",
"function ${TM_FILENAME_BASE}(props: Props) {",
" return (",
" <${TM_FILENAME_BASE}Style></${TM_FILENAME_BASE}Style>",
" );",
"}",
"",
"const ${TM_FILENAME_BASE}Style = styled.div``;",
"",
"export default ${TM_FILENAME_BASE};"
],
"description": "Styled-components 기본 컴포넌트 자동 생성 (파일명 기반)"
}
}
Button 테마 작성
theme.ts
export type ButtonSize = "large" | "medium" | "small";
export type ButtonScheme = "primary" | "normal";
button: {
[key in ButtonSize] : {
fontSize: string;
padding: string;
}
};
buttonScheme: {
[key in ButtonScheme]: {
color: string;
backgroundColor: string;
};
};
- 버튼 사이즈, 스키마 타입 지정
button: {
large: {
fontSize: '1.5rem',
padding: '1rem 2rem',
},
medium: {
fontSize: '1rem',
padding: '0.5rem 1rem',
},
small: {
fontSize: '0.75rem',
padding: '0.25rem 0.5rem',
},
},
buttonScheme: {
primary: {
color: 'white',
backgroundColor: 'midnightblue',
},
normal: {
color: 'black',
backgroundColor: 'lightgray',
}
}
- light 테마에 button, buttonScheme 지정
Button.tsx에서 Props 설정
interface Props {
children: React.ReactNode;
size: ButtonSize;
scheme: ButtonScheme; // 기본적인 디자인 프리셋
disabled?: boolean;
isLoading?: boolean; // 버튼 여러번 클릭 안되도록
}
function Button({ children, size, scheme,
disabled, isLoading }: Props) {
return (
<ButtonStyle></ButtonStyle>
)
}
태그에 Props 넘겨주기
function Button({ children, size, scheme,
disabled, isLoading }: Props) {
return (
<ButtonStyle size={size} scheme={scheme}
disabled={disabled} isLoading={isLoading}
>
{children}
</ButtonStyle>
);
}
ButtonStyle 작성
const ButtonStyle = styled.button<Omit<Props, "children">>`
font-size: ${({theme, size}) => theme.button[size].fontSize};
color: ${({theme, scheme}) => theme.buttonScheme[scheme].color};
background-color: ${({theme, scheme}) => theme.buttonScheme[scheme].backgroundColor};
border: 0;
border-radius: ${({theme}) => theme.borderRadius.default};
padding: ${({theme, size}) => theme.button[size].padding};
opacity: ${({disabled}) => disabled ? 0.5 : 1};
pointer-events: ${({disabled}) => disabled ? "none" : "auto"};
cursor: ${({disabled}) => disabled ? "default" : "pointer"};
`;
- styled.div => styled.button으로 변경
Home.tsx에 Button 추가
<>
<Title size="medium" color="background">
제목 테스트
</Title>
<Button size="large" scheme="normal">버튼 테스트</Button>
<div>home body</div>
</>
Button 컴포넌트 테스트 작성하기
Button.spec.tsx
import { render, screen } from "@testing-library/react";
import Button from "./Button";
import { BookStoreThemeProvider } from "../../context/ThemeContext";
describe("Button 컴포넌트 테스트", () => {
it('렌더를 확인', () => {
// 1. 렌더 : 가상화면 렌더됨
// 적적한 props 넣어줘야 함
render(
<BookStoreThemeProvider>
<Button size="large" scheme="primary">버튼</Button>
</BookStoreThemeProvider>
);
// 2. 확인 : '버튼'이란 텍스트 화면상에 있는지 확인
expect(screen.getByText("버튼")).toBeInTheDocument();
});
it('size props 적용', () => {
const {container} = render(
<BookStoreThemeProvider>
<Button size="large" scheme="primary">버튼</Button>
</BookStoreThemeProvider>
);
expect(screen.getByRole("button")).toHaveStyle({
fontSize: "1.5rem"
});
});
})
- size props 적용 검사할 때 screen.getByrole("button") 사용
- getByrole : HTML의 role 속성에 따라 요소를 찾아내는 데 사용
input 컴포넌트 생성
forwardRef 방식으로 생성
interface Props {
placeholder?: string;
}
// 포워드 ref 방식
const InputText = React.forwardRef(
({placeholder}: Props, ref:ForwardedRef<HTMLInputElement>) => {
return (
<InputTextStyle placeholder={placeholder} ref={ref} />
)
})
2025.11.25 - [프레임워크\라이브러리/React] - [React] forwardRef란?
[React] forwardRef란?
forwardRef() 란?React 공식문서는, 일부 컴포넌트가 수신한 ref를 받아 조금 더 아래로 전달하는 Opt-in 기능이라고 소개한다.React 컴포넌트를 forwardRef() 메서드로 감싸면, 컴포넌트 함수는 추가적으로
thinktank911.tistory.com
Home컴포넌트에 추가
<InputText placeholder="여기에 입력하세요"/>
<div>home body</div>
InputText 컴포넌트 테스트 작성하기
InputText.spec.tsx
import { render, screen } from "@testing-library/react";
import InputText from "./InputText";
import { BookStoreThemeProvider } from "../../context/ThemeContext";
describe("InputText 컴포넌트 테스트", () => {
it('렌더를 확인', () => {
// 1. 렌더 : 가상화면 렌더됨
// 적적한 props 넣어줘야 함
render(
<BookStoreThemeProvider>
<InputText placeholder="여기에 입력" />
</BookStoreThemeProvider>
);
// 2. 확인 : '여기에 입력'이란 플레이스홀더 텍스트 화면상에 있는지 확인
expect(screen.getByPlaceholderText("여기에 입력")).toBeInTheDocument();
});
})
- getByPlaceholderText 활용
it('forwardRef 테스트', () => {
const ref = React.createRef<HTMLInputElement>();
render(
<BookStoreThemeProvider>
<InputText placeholder="여기에 입력" ref={ref} />
</BookStoreThemeProvider>
);
// ref.current : ref는 항상 current로 액세스함
expect(ref.current).toBeInstanceOf(HTMLInputElement);
});
- forwardRef 테스트 작성
- createRef 로 ref 생성
- ref.current : ref는 항상 current로 액세스함
헤더와 푸터
헤더와 푸터의 구성

헤더 만들기
Header.tsx
- 로고 이미지 배치
- 카테고리 배치 : CATEGORY 상수를 map으로 돌려 배치
- 개인화 영역 배치
- HeaderStyle 작성
import { styled } from "styled-components";
import logo from "../../assets/images/logo192.png";
const CATEGORY = [
{
id: null,
name: "전체"
},
{
id: 0,
name: "동화"
},
{
id: 1,
name: "소설"
},
{
id: 2,
name: "사회"
},
]
function Header(){
return (
<HeaderStyle>
<h1 className="logo">
<img src={logo} alt="book store"/>
</h1>
{/* 카테고리 영역 */}
<nav className="category">
<ul>
{CATEGORY.map((item) => (
<li key={item.id}>
<a href={item.id === null ? `/books` :
`/books?category_id=${item.id}`}>
{item.name}
</a>
</li>
))}
</ul>
</nav>
{/* 개인화 영역 */}
<nav className="auth">
<ul>
<li>
<a href="/login">로그인</a>
</li>
<li>
<a href="/signup">회원가입</a>
</li>
</ul>
</nav>
</HeaderStyle>
)
}
const HeaderStyle = styled.header`
width: 100%;
margin: 0 auto;
max-width: ${({theme}) => theme.layout.width.large};
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 0;
border-bottom: 1px solid ${({theme}) => theme.color.background};
.logo {
img {
width: 100px;
}
}
.category {
ul {
display: flex;
gap: 32px;
li {
a {
font-size: 1.5rem;
font-weight: 600;
text-decoration: none;
color: ${({theme}) => theme.color.text};
&:hover {
color: ${({theme}) => theme.color.primary};
}
}
}
}
}
.auth {
ul {
display: flex;
gap: 16px;
li {
a {
font-size: 1rem;
font-weight: 600;
text-decoration: none;
}
}
}
}
`;
export default Header;
로그인 회원가입 아이콘 배치
- react icons 모듈 설치 후 사용 : npm install react-icons --save
[이슈]ERROR in src/components/common/Header.tsx:48:29 TS2786: 'FaSignInAlt' cannot be used as a JSX component. Its return type 'ReactNode' is not a valid JSX element. Type 'undefined' is not assignable to type 'Element | null'. 46 | <li> 47 | <a href="/login"> > 48 | <FaSignInAlt/> | ^^^^^^^^^^^ 49 | 로그인 50 | </a> 51 | </li> ERROR in src/components/common/Header.tsx:54:30 TS2786: 'FaRegUser' cannot be used as a JSX component. Its return type 'ReactNode' is not a valid JSX element. 52 | <li> 53 | <a href="/signup"> > 54 | <FaRegUser/> | ^^^^^^^^^ 55 | 회원가입 56 | </a> 57 | </li> - 문제발생 : <FaSignInAlt> 와 <FaRegUser> 를 react-icons에서 호출하려고 하는데 해당 에러가 발생한다.
- 원인파악 : react-icons와 React 타입 버전이 맞지 않을 때 발생
- 해결방안
1) react-icons 업데이트
2) TypeScript와 React 타입 업데이트
npm install react-icons@latest
npm install @types/react@latest @types/react-dom@latest typescript@latest
푸터 만들기
- 헤더와 거의 동일
import styled from "styled-components"; import logo from "../../assets/images/logo192.png"; function Footer(){ return( <FooterStyle> <h1 className="logo"> <img src={logo} alt="book store"/> </h1> <div className="copyright"> <p> copyright(c), 2024, book store. </p> </div> </FooterStyle> ) } const FooterStyle = styled.footer` width: 100%; margin: 0 auto; max-width: ${({theme}) => theme.layout.width.large}; padding: 20px 0; border-top: 1px solid ${({theme}) => theme.color.background}; display: flex; justify-content: space-between; .logo { img { width: 140px; } } .copyright { p { font-size: 0.75rem; color: ${({theme}) => theme.color.text}; } } `; export default Footer;
바디 만들기
레이아웃에서 설정
interface LayoutProps {
children: React.ReactNode; // 리액트로 만든 모든 컴포넌트들 선언할 수 있다.
}
function Layout({children} : LayoutProps ){
return (
<>
<Header />
<LayoutStyle>
{children}
</LayoutStyle>
<Footer />
</>
)
}
const LayoutStyle = styled.main`
width: 100%;
margin: 0 auto;
max-width: ${({theme}) => theme.layout.width.large};
padding: 20px 0;
`;
export default Layout;
'프로젝트 > BookStore 사이트' 카테고리의 다른 글
| 1128 도서 상세 페이지 구현 (0) | 2025.11.28 |
|---|---|
| 1127 비밀번호 초기화, 도서 목록 페이지 구현 (0) | 2025.11.27 |
| 1126 라우트, 회원가입 (0) | 2025.11.26 |
| 1124 레이아웃 구성 및 테마 적용 - 전역 스타일 생성, 로컬스토리지 저장 (0) | 2025.11.24 |
| 1121 book store 프로젝트 시작 (0) | 2025.11.21 |