key와 children속성 이해하기

김동현·2023년 8월 7일
0

key 속성 설정하기

function App() {
  const texts = [<p>Hello</p>, <p>World</p>]
  return <div className="App">{texts}</div>
}

위의 코드를 실행하면 브라우저에 이러한 경고문이 뜬다.

이 경고는 key 속성을 설정해주면 사라진다.

function App() {
  const texts = [<p key="1">Hello</p>, <p key="2">World</p>]
  return <div className="App">{texts}</div>
}

리액트 컴포넌트의 Attributes 타입 선언문을 보자.

interface Attributes {
  key?: Key | null | undefined;
}

경고문을 표시할 정도라면 그냥 필수 속성으로 해놓지 왜 선택 속성으로 해놓았을까?
아, 그러면 배열에 담겨있지 않은 컴포넌트에도 일일이 key 속성을 추가해줘야 하네...

Key는 어떠한 타입인지 보자.

type Key = string | number;

숫자나 문자열 둘 중 하나로 표현된다.

function App() {
  const texts = ['hello', 'world'].map((text, index) => <p key={index}>{text}</p>)
  return <div className="App">{texts}</div>
}

map 의 콜백함수에는 index 매개변수가 제공되는데 이를 이용하여 key 값을 설정할 수 있다.

그런데 이러한 방식은 비추한다.

리액트는 컴포넌트에 설정된 key로 컴포넌트를 구분한다.

['hello', 'world'].map((text, index) => <p key={index}>{text}</p>)

위와 같은 컴포넌트가 있을 때 반환값은 아래와 같다.

<p key=0>hello</p>
<p key=1>world</p>

hello 컴포넌트에 key가 0, world 컴포넌트에 key가 1가 대응된다.

동적으로 배열에 아이템을 추가했다고 가정해보자.

['hello', 'world', 'people'].map((text, index) => <p key={index}>{text}</p>)
<p key=0>hello</p>
<p key=1>world</p>
<p key=2>people</p>

리액트는 key가 0, 1인 컴포넌트는 변한것이 없다고 판단해서 key가 2인 컴포넌트만 랜더링한다.
하지만 배열에 아이템을 뒤쪽이 아닌 앞쪽으로 추가했다고 가정해보자.

['people', 'hello', 'world'].map((text, index) => <p key={index}>{text}</p>)
<p key=0>people</p>
<p key=1>hello</p>
<p key=2>world</p>

리액트는 key가 0, 1인 컴포넌트가 변했다고 판단해서 0, 1, 2인 키를 가진 모두를 재 랜더링한다.
아이템이 만개가 넘는다고 가정한다면 성능의 저하가 일어날 수있다.

순서에 따라 달라지는 key가 아닌 아이템별로 고유한 key값을 설정하는 것이 필수다.

children 속성 설정하기

children 속성의 타입을 보자.

children?: ReactNode | undefined;

아 물론, children 은 자식 요소를 포함할 수 있는 컴포넌트에서만 사용할 수 있다.
children 속성에 자식 요소를 설정할땐 다음과 같이 사용한다.

function App() {
  const texts = ['hello', 'world'].map((text, index) => <p key={index} children={text} />)
  return <div className="App" children={texts} />
}

컴포넌트 내부에서 children 속성 사용하기

위에서는 리액트 컴포넌트인 pchildren 속성을 설정했다.
리액트 컴포넌트를 수정하는건 좀 그러니 사용자 컴포넌트 P 를 만들고 Pchildren 속성을 설정해보자.

function App() {
  const texts = ['hello', 'world'].map((text, index) => <P key={index} children={text} />)
  return <div className="App" children={texts} />
}

그리고 P 에서 children 속성이 어떻게 사용되는지 알아보자.

function 키워드 함수

type PProps = {
  children?: ReactNode
}
function P(props: PProps) {
  const {children} = props
  return <p children={children} />
}

화살표 함수

type PProps = {
  children?: ReactNode
}
const P: FC<PProps> = props => {
  const {children} = props
  return <p children={children} />
}
// 또는
const P: FC<PProps> = ({children}) => {
  return <p children={children} />
}

PropsWithChildren 타입과 children 속성

리액트18 버전부터는 FC 타입에서 children 속성을 제거하고 PropsWithChildren 이라는 새로운 제네릭 타입을 제공한다.
따라서 리액트18 버전부터는 아래와 같이 사용하도록 하자.

import type {FC, PropsWithChildren} from 'react'

type PProps = {}

const P: FC<PropsWithChildren<PProps>> = ({children}) => {
  return <p children={children} />
}

그런데 이렇게 따로 정의되어 있는 타입을 사용할 때는 어떠한 속성이 들어있는지 명확하게 파악하기가 힘들다.
위에만 보아도 PropsWithChildren 타입이 childrend 속성을 포함시켰다.
위의 PropsWithChildren<PProps> 타입의 경우엔 그래도 좀 간단한 편이라서 괜찮지만 다른 타입들을 props로 사용한다고 했을 때 각각의 속성들을 매개변수에 입력해주지 않으면 타입에러가 난다.

따라서 그냥 props 라는 하나의 객체로 받거나 나머지 매개변수를 사용하면된다.

const P: FC<PropsWithChildren<PProps>> = ({children, ...props}) => {
  return <p children={children} />
}

JSX {...props} 구문

JSX는 {...props} 구문을 제공한다.
바로 위의 코드에서 매개변수 propsp 컴포넌트의 속성으로 그대로 전달하고 싶을 때 사용한다.

const P: FC<PropsWithChildren<PProps>> = ({children, ...props}) => {
  return <p children={children} {...props} />
}

클래스 컴포넌트

type PProps = {}
class P extends Component<PropsWithChildren<PProps>> {
  render(): React.ReactNode {
    const {children} = this.props
    return <p children={children} />
  }
}
profile
프론트에_가까운_풀스택_개발자

0개의 댓글