웹풀스택 일일정리

13주차-파트02: 선택적 속성, styled-components, Global style, theme, Record타입, Generic, 오버라이딩 후 재정의, Omit, 테스트,인덱스 시그니처와 맵드 타입, children, React.ReactNode, React.FC, forwardRef, CSS: margin, justify-content, 리액트 아이콘

ddodoi 2024. 11. 12. 21:46

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

  1. 프로젝트 전체에 일관된 스타일링을 적용
  2. "user agent stylesheet"로 표시되는 브라우저의 기본 스타일이 차이를 만든다.
  3. 브라우저 간 스타일 차이를 극복하기 위해 사용
  4. 예시) 에릭마이어의 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(테마)

styled-components 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를 사용하면 모든 렌더링 가능한 요소를 안전하게 처리할 수 있다.

다음처럼 children의 타입이 미정이기 때문에 React Node로 미리 타입을 안전하게 지정해줄 수 있다.

 

 

 

children component의 타입

 

<렌더링 가능한 타입>

  • 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;

 

사용 이유

  1. Props 타입 정의: 컴포넌트가 받을 Props의 타입을 지정할 수 있다.
  2. 함수형 컴포넌트임을 명시: React의 함수형 컴포넌트로 작성된 것을 명확히 표현한다.
  3. children 자동 포함: React.FC를 사용하면 기본적으로 children 프로퍼티가 포함된다.

 

※ React.FC의 단점과 주의사항

  1. 불필요한 children 포함: React.FC를 사용하면 children이 기본적으로 포함되는데, 만약 컴포넌트가 children을 받지 않아야 한다면 불필요한 타입이 추가될 수 있다.
  2. 기본 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} />;
  }
);

 

🛠️동작 과정

  1. React.forwardRef는 함수형 컴포넌트를 감싸는 고차 함수(Higher-Order Function, HOF)이다.
  2. 이 함수형 컴포넌트의 첫 번째 매개변수는 일반적인 props이고, 두 번째 매개변수로 ref를 받는다.
  3. 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;

 

 

🛠️동작 과정

  1. useRef를 사용하여 inputRef를 생성함.
  2. InputText 컴포넌트에 ref={inputRef}를 전달함.
  3. 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

콘텐츠 항목 사이와 주위에 공간을 분배하는 방법

 

https://velog.io/@cherry_eong/CSS-justify-content-%EC%86%8D%EC%84%B1%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90

 

[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