13주차-파트02: 선택적 속성, styled-components, Global style, theme, Record타입, Generic, 오버라이딩 후 재정의, Omit, 테스트,인덱스 시그니처와 맵드 타입, children, React.ReactNode, React.FC, forwardRef, CSS: margin, justify-content, 리액트 아이콘
CHAPTER 1. 선택적 속성, styled-components, Global style, theme, 인덱스 시그니처와 맵드 타입, 제너릭(Generic)
✔️선택적 속성
선택적 속성은 필수가 아니며, 컴포넌트를 사용할 때 전달하지 않아도 문제가 발생하지 않는다.
interface Props {
children: React.ReactNode; // 필수 속성
size: HeadingSize; // 필수 속성
color?: ColorKey; // 선택적 속성
}
✅styled-components
CSS-in-JS 방식으로, JavaScript 파일 안에서 CSS를 작성하는 스타일링 방법을 제공한다.
타입스크립트에서의 styled-components는 컴포넌트와 props에 대한 타입 정의가 추가된다.
styled 뒤에 오는 것은 HTML 태그이다. 예를 들어, styled.button, styled.div, styled.h1 등은 각각 HTML의 <button>, <div>, <h1> 태그를 스타일링한 컴포넌트를 생성한다. styled 객체는 HTML 태그 이름을 메서드처럼 제공하여 해당 태그를 기반으로 스타일링된 컴포넌트를 반환한다.
🛠️styled-components 설치
npm install styled-components
import하여 다음처럼 사용한다.
import styled from 'styled-components';
interface ButtonProps {
primary?: boolean; // 선택적 props
}
const Button = styled.button<ButtonProps>`
background-color: ${(props) => (props.primary ? "blue" : "gray")};
color: white;
padding: 10px 20px;
border: none;
border-radius: 5px;
`;
function App() {
return (
<>
<Button primary>Primary Button</Button>
<Button>Secondary Button</Button>
</>
);
}
✅Global Style
- 프로젝트 전체에 일관된 스타일링을 적용
- "user agent stylesheet"로 표시되는 브라우저의 기본 스타일이 차이를 만든다.
- 브라우저 간 스타일 차이를 극복하기 위해 사용
- 예시) 에릭마이어의 reset css, normalize.css, sanitize.css => 브라우저마다 다른 기본 스타일을 제거하고 웹 브라우저와 상관없이 초기 스타일을 통일시켜 준다.
본 프로젝트는 sanitize.css를 사용하여 global style을 구현하였다. 다음은 style>global.ts파일이다.
import "sanitize.css";
import { createGlobalStyle } from "styled-components";
export const GlobalStyle = createGlobalStyle`
body{
padding: 0;
margin:0;
}
h1{
margin:0;
}
`;
App.tsx파일로 import해와 프로젝트 전체에 적용해주었다.
✅theme(테마)
theme의 값들도 타입으로 관리해주는 것이 좋다.
interface Theme {
name: string;
color: {
primary: string;
background: string;
secondary?: string;
};
}
export const light = {
name: "light",
color: {
primary: "brown",
background: "lightgrey",
},
};
color 안에 추가적인 값들이 추가될수도 있으므로 다음처럼 표현하였다.
interface Theme {
name: string;
color: {
[key: string]: string;
};
}
리액트의 styled-component에서 제공하는 Theme provider를 import하여 theme를 적용 시킬수 있다.
ThemeProvider는 반드시 내부에 children을 포함해야 한다!
📌 ThemeProvider는 반드시 닫는 태그(</ThemeProvider>)로 감싸야 정상 동작한다.
App.tsx에서 다음처럼 theme를 적용시켜보자.
✔️ 인덱스 시그니처(Index Signature)와 맵드 타입(Mapped Type)을 이용하여 타입 정의
ButtonScheme의 Mapped Type)
type ButtonScheme = "primary" | "secondary" | "danger";
buttonScheme: {
[key in ButtonScheme]: {
color: string;
backgroundColor: string;
}
}
맵드 타입을 사용하여, 특정 타입의 모든 값을 키로 가지는 객체를 생성할 수 있다. 이 코드에서 buttonScheme은 ButtonScheme의 각 값에 대해 { color, backgroundColor } 구조를 가지는 객체를 생성한다.
위 코드의 최종 객체는 다음과 같이 구성된다.
buttonScheme: {
primary: {
color: string;
backgroundColor: string;
};
secondary: {
color: string;
backgroundColor: string;
};
danger: {
color: string;
backgroundColor: string;
};
};
✔️타입스크립트의 Record타입
Record<K, T>는 TypeScript에서 제공하는 유틸리티 타입(Utility Type) 중 하나로, 특정 키(K)에 대해 값(T)을 매핑하는 객체 타입을 정의할 때 사용된다.
type Record<K extends keyof any, T> = {
[P in K]: T;
};
즉, Record<K, T>는:
- K : 객체의 키(key) 타입 (문자열, 숫자, 심볼이 될 수 있음)
- T : 객체의 값(value) 타입
예제)
1.
type UserRoles = Record<string, string>;
const roles: UserRoles = {
admin: "관리자",
user: "사용자",
guest: "손님"
};
2. 다음처럼 활용할수도 있다.
type UserRole = "admin" | "editor" | "viewer";
type RoleDescriptions = Record<UserRole, string>;
const roleDesc: RoleDescriptions = {
admin: "전체 관리 권한",
editor: "컨텐츠 수정 권한",
viewer: "읽기 전용 권한"
};
✔️제너릭(Generic)
TypeScript에서 코드의 재사용성과 타입 안정성을 극대화하기 위해 사용하는 기능이다. 제네릭은 특정 타입에 의존하지 않고 다양한 타입을 처리할 수 있는 컴포넌트를 작성하는 데 유용하다.
<함수에서의 제너릭>
function identity<T>(value: T): T {
return value;
}
설명:
- identity는 제네릭 함수입니다.
- **<T>는 타입 변수(Type Variable)**로, 함수 호출 시 결정됩니다.
- value는 **인자(argument)**로 전달받으며, 타입은 T에 의해 결정됩니다.
- 함수의 반환 타입도 동일하게 T입니다.
예시)
const num = identity<number>(42); // T가 number로 결, 전달인자는 42
const str = identity<string>("Hello"); // T가 string으로 결정, 전달인자는 "Hello"
CHAPTER 2. 오버라이딩 후 재정의, Omit, 테스트
light와 dark 테마에 중복되는 부분 => light테마 오버라이딩 후 name과 color만 재정의
export const light : Theme = {
name: 'light',
color : {
primary : "brown",
background : "lightgray",
secondary : "blue",
third: "green",
},
heading : {
large : {
fontSize: "2rem"
},
medium : {
fontSize : "1.5rem"
},
small : {
fontSize : "1rem"
}
},
};
export const dark : Theme = {
...light,
name: 'dark',
color : {
primary : "coral",
background : "midnightblue",
secondary : "darkblue",
third: "darkgreen",
},
};
✔️Omit
TypeScript에서 제공하는 유틸리티 타입(Utility Type) 중 하나로, 특정 타입에서 일부 속성을 제거한 새로운 타입을 생성할 때 사용
Omit<Type, Keys>
- Type: 원본타입
- Keys: 제거하고자 하는 속성(키)들의 집합
원본 타입에서 지정된 키(Keys)를 제외한 나머지 속성만 포함하는 새로운 타입을 생성
예시)
interface Props {
title: string;
color: string;
children: React.ReactNode;
}
type OmittedProps = Omit<Props, "children">;
// OmittedProps는 아래와 같이 정의됩니다:
{
title: string;
color: string;
}
Omit<Props, "children">을 사용하여 children 속성을 제외한 타입을 만든다.
🌱테스트해보기
import {render, screen} from '@testing-library/react';
import Title from "./Title";
import { BookStoreThemeProvider } from '../../context/themeContext';
describe("Title 컴포넌트 테스트", ()=>{
it ('렌더를 확인', () =>{
//1.렌더
render(
<BookStoreThemeProvider>
<Title size = "large">제목</Title>
</BookStoreThemeProvider>);
//2. 확인
expect(screen.getByText("제목")).toBeInTheDocument();
});
it ('size props 적용', () => {
const {container} = render (
<BookStoreThemeProvider>
<Title size = "medium">제목</Title>
</BookStoreThemeProvider>
);
expect (container?.firstChild).toHaveStyle({
fontSize: "2rem"
});
})
it ('color props 적용', () => {
const {container} = render(
<BookStoreThemeProvider>
<Title size = "large" color = "primary">제목</Title>
</BookStoreThemeProvider>
)
})
});
npm run test
CHAPTER 3. children, React.ReactNode, React.FC, forwardRef
✅리액트의 children
컴포넌트 태그 내부에 작성된 모든 JSX 요소 또는 텍스트.
예시)
<Button>
Click Me
</Button>
Button 컴포넌트는 Click Me라는 텍스트를 children으로 받는다.
<Children을 전달하는 방법 2가지>
1) props형태로 받는 방법
function App() {
return (
<>
<Layout children={<Home />}></Layout>
</>
);
}
2) 태그 사이에 넣기
function App() {
return (
<>
<Layout>
<Home />
</Layout>
</>
);
}
✔️React.ReactNode
React.ReactNode는 React에서 지원하는 모든 렌더링 가능한 요소를 나타내는 타입으로 텍스트, JSX, 숫자, 배열 등 다양한 타입의 데이터를 받을 수 있도록 설계되었다. React에서는 children의 타입을 명시하지 않으면, 다양한 데이터 형식을 처리하기 어려워질 수 있다. React.ReactNode를 사용하면 모든 렌더링 가능한 요소를 안전하게 처리할 수 있다.
<렌더링 가능한 타입>
- JSX요소 ex) <div>Hello</div>
- 문자열 또는 숫자
- 배열
- null 또는 undefined
- React.Fragment ex) <>
<div>Item 1</div>
<div>Item 2</div>
</>
※ React.ReactNode vs ReactElement
- React.ReactNode: JSX 요소뿐만 아니라 문자열, 숫자, 배열 등 다양한 데이터 타입을 포함.
- React.ReactElement: React에서 렌더링 가능한 단일 JSX 요소만을 의미.
- 예: <div>Hello</div>는 React.ReactElement 타입.
✅React.FC
React에서 함수형 컴포넌트를 작성할 때 사용하는 타입이다. TypeScript를 사용할 때 컴포넌트의 타입을 명시적으로 지정하기 위해 활용된다. React.FC는 Function Component를 의미하며, 컴포넌트의 기본적인 구조와 Props 타입을 정의하는 데 유용하다.
예시)
import React from 'react';
// Props 타입 정의
interface MyComponentProps {
title: string;
count?: number; // 선택적 Props
}
// React.FC 사용
const MyComponent: React.FC<MyComponentProps> = ({ title, count, children }) => {
return (
<div>
<h1>{title}</h1>
<p>Count: {count}</p>
{children && <div>{children}</div>}
</div>
);
};
export default MyComponent;
사용 이유
- Props 타입 정의: 컴포넌트가 받을 Props의 타입을 지정할 수 있다.
- 함수형 컴포넌트임을 명시: React의 함수형 컴포넌트로 작성된 것을 명확히 표현한다.
- children 자동 포함: React.FC를 사용하면 기본적으로 children 프로퍼티가 포함된다.
※ React.FC의 단점과 주의사항
- 불필요한 children 포함: React.FC를 사용하면 children이 기본적으로 포함되는데, 만약 컴포넌트가 children을 받지 않아야 한다면 불필요한 타입이 추가될 수 있다.
- 기본 Props 타입 정의 방식과 충돌 가능성: defaultProps와 관련된 타입 추론에 문제가 생길 수 있다.
따라서 다음처럼 React.FC를 사용하지 않고 함수형 컴포넌트를 작성할 수도 있다.
import React from 'react';
// Props 타입 정의
interface MyComponentProps {
title: string;
count?: number;
}
// React.FC 대신 사용
const MyComponent = ({ title, count }: MyComponentProps) => {
return (
<div>
<h1>{title}</h1>
<p>Count: {count}</p>
</div>
);
};
export default MyComponent;
⭐결론
- React.FC를 사용해야 하는 경우: children을 기본적으로 포함하거나 컴포넌트의 함수형 특성을 명시적으로 표현하고 싶을 때.
- React.FC를 사용하지 않는 경우: 더 간단한 코드 작성과 children의 명시적인 제어가 필요할 때.
✅forwardRef
forwardRef는 React의 고급 기능으로, 부모 컴포넌트에서 자식 컴포넌트 내부의 DOM 엘리먼트 또는 클래스 인스턴스에 접근할 수 있도록 해준다.일반적으로 부모에서 ref를 자식에게 전달하려고 하면, 기본적으로는 전달되지 않는데, forwardRef를 사용하면 이것이 가능해진다.
✈️ 왜 필요한가?
일반적으로 컴포넌트는 부모가 자식의 내부 DOM에 직접 접근할 수 없도록 캡슐화된다. 하지만 어떤 경우에는 다음 예시처럼 DOM 노드에 접근해야 할 필요가 있다:
- 특정 엘리먼트에 포커스를 주기 위해.
- DOM 엘리먼트의 크기 또는 위치 정보를 읽어오기 위해.
- DOM 엘리먼트를 직접 조작해야 하는 경우.
forwardRef는 이런 상황에서 사용된다.
1. 코드 예시
다음은 실습의 InputText.tsx파일에서 forwardRef를 이용하여 Input 컴포넌트를 구현한 부분이다.
const InputText = React.forwardRef(
({ placeholder }: Props, ref: ForwardedRef<HTMLInputElement>) => {
return <InputTextStyle placeholder={placeholder} ref={ref} />;
}
);
🛠️동작 과정
- React.forwardRef는 함수형 컴포넌트를 감싸는 고차 함수(Higher-Order Function, HOF)이다.
- 이 함수형 컴포넌트의 첫 번째 매개변수는 일반적인 props이고, 두 번째 매개변수로 ref를 받는다.
- ref는 InputTextStyle (styled.input)에 직접 전달되어, 부모 컴포넌트가 InputText를 사용할 때 이 input 태그에 직접 접근할 수 있게 된다.
2. 부모 컴포넌트에서 사용하는 법
부모 컴포넌트에서 InputText를 사용할 때 ref를 전달하면, 실제 DOM 노드인 <input>에 접근할 수 있다.
import React, { useRef } from "react";
import InputText from "./InputText";
const ParentComponent = () => {
const inputRef = useRef<HTMLInputElement>(null);
const focusInput = () => {
if (inputRef.current) {
inputRef.current.focus(); // input 요소에 포커스를 줌
}
};
return (
<div>
<InputText ref={inputRef} placeholder="Enter text..." />
<button onClick={focusInput}>Focus Input</button>
</div>
);
};
export default ParentComponent;
🛠️동작 과정
- useRef를 사용하여 inputRef를 생성함.
- InputText 컴포넌트에 ref={inputRef}를 전달함.
- focusInput 함수에서 inputRef.current.focus()를 호출하면, InputText 내부의 <input> 요소에 포커스를 줄 수 있음.
✔️useRef<HTMLInputElement>(null)
const inputRef = useRef<HTMLInputElement>(null);
- useRef는 React에서 DOM 요소나 컴포넌트 인스턴스를 직접 참조할 때 사용하는 Hook이다.
- useRef<HTMLInputElement>(null)는 초기값이 null인 inputRef를 생성하는 코드야.
- <HTMLInputElement>는 TypeScript의 제네릭(Generic) 타입으로, inputRef가 input 요소를 가리키도록 명시해 준 거야.
- 이렇게 하면 TypeScript가 inputRef.current를 HTMLInputElement | null 타입으로 인식해서 자동 완성을 제공하고 타입 안전성을 보장해 줘.
3. forwardRef가 필요한 이유
일반적으로 부모 컴포넌트가 자식 컴포넌트 내부의 DOM 요소에 접근하려면 ref를 사용할 수 없. 예를 들어, 아래처럼 forwardRef 없이 만들면 ref가 undefined가 되어버린다.
CHAPTER 4. CSS: margin, justify-content
✅margin
요소와 요소 사이의 외부 여백을 정의하는 속성
⭐자주 쓰이는 margin 값
- margin: 0; 위아래, 좌우 여백 지정 x
- margin: 10px 20px 15px 5px; 4개 값: 위, 오른쪽, 아래, 왼쪽(위에서부터 시계방향)
- margin: 10px 20px 15px; 3개 값: 위, 좌우, 아래
- margin: 10px 20px; 2개 값: 상하, 좌우
- margin: 10px; 1개 값: 모든 방향
- margin: auto; 마진을 자동으로 설정하여 부모 컨테이너 내에서 요소를 수평으로 가운데 정렬할 때 자주 사용
✔️justify-content
콘텐츠 항목 사이와 주위에 공간을 분배하는 방법
[CSS] justify-content 속성
justify-content는 콘텐츠 항목 사이와 주위에 공간을 분배하는 방법을 정의한다.출처 : 1분코딩
velog.io
CHAPTER 5. React icons
🌱리액트 아이콘
https://react-icons.github.io/react-icons/
React Icons
react-icons.github.io
사용하려면 설치해야한다.
npm install react-icons --save