Ayden's journal

React 컴포넌트

리액트 컴포넌트

리액트 엘리먼트의 재사용성을 높이고, 다양한 속성들을 자유롭게 사용하기 위해 이러한 엘리먼트를 특정한 함수의 리턴 값으로 사용하게 되는데, 이를 리액트 컴포넌트라고 부른다. 이러한 컴포넌트의 경우 함수 이름을 통해 하나의 태그처럼 활용할 수가 있게 된다. 리액트 컴포넌트를 생성하기 위해서는 반드시 함수의 첫 글자를 대문자로 작성해주어야 한다.

import ReactDOM from 'react-dom';

const Hello = () => <h1>Hello World</h1>;

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <>
    <Hello />
    <Hello />
    <Hello />
  </>
);

이런 특성을 모듈 문법으로 활용하면 컴포넌트 특성에 집중해서 훨씬 더 독립적인 코드를 작성할 수가 있다.

import diceBlue01 from './assets/dice-blue-1.svg';
import diceBlue02 from './assets/dice-blue-2.svg';

function Dice() {
  return (
    <>
      <img src={diceBlue01} alt="주사위" />;
      <img src={diceBlue02} alt="주사위" />;
    </>
  )
}

export default Dice;

리액트에서 이미지 파일을 사용하려면 import로 가져와서 중괄호로 감싸줘야 한다. 또, 리턴 부분을 소괄호로 감싸주게 되면 여러줄에 걸쳐서 코드 작성이 가능해진다.

 

props

리액트 컴포넌트에 임의의 속성을 지정하면, 각 속성이 하나의 객체로 모여서 컴포넌트를 정의한 함수의 첫 번째 파라미터로 전달된다. 그래서 컴포넌트를 활용할 때 속성값을 다양하게 전달하고 이 props 값을 활용하면, 똑같은 컴포넌트라도 전달된 속성값에 따라 서로 다른 모습을 그려낼 수 있게 된다.

import diceBlue01 from './assets/dice-blue-1.svg';
import diceBlue02 from './assets/dice-blue-2.svg';
// ...
import diceRed01 from './assets/dice-red-1.svg';
import diceRed02 from './assets/dice-red-2.svg';
// ...

const DICE_IMAGES = {
  blue: [diceBlue01, diceBlue02],
  red: [diceRed01, diceRed02],
};

function Dice({ color = 'blue', num = 1 }) {
  const src = DICE_IMAGES[color][num - 1];
  const alt = `${color} ${num}`;
  return <img src={src} alt={alt} />;
}

<Dice color="blue" num={2} />

Dice 컴포넌트에서 color와 num이라는 props를 받으면, 이 값들에 따라 완전히 다른 이미지 파일을 랜더링할 수 있게 만들 수도 있다. 이처럼 하나의 객체로 필요한 데이터를 정리해놓고 특정한 컴포넌트의 조합으로 이 데이터를 가져오는 것이 굉장히 중요한 리액트의 기술이라고 생각한다.

 

props 중에서는 children이라고 하는 독특한 속성이 하나 존재한다. 리액트 컴포넌트를 마치 HTML 태그처럼 여닫는 블럭을 생성하고, 그 안에 내용을 입력하면 이 내용은 children prop로 컴포넌트 함수에서 사용이 가능하다.

const Button = ({ children }) => <button>{children}</button>

<Button>끝내기</Button>

 

state

state는 리액트에서 이벤트 등에 의해 변경되는 동적인 상태를 지속적으로 추적할 수 있는 변수값이다. 이 값이 바뀔 때마다 화면을 새롭게 랜더링한다. 함수형 컴포넌트에서 state를 사용하기 위해서는 useState라는 함수를 활용해야 한다.

import { useState } from 'react';

const [num, setNum] = useState(1);

useState를 사용하면 변경된 값을 담아두는 변수와 변경할 값을 설정해주는 setter 함수가 배열의 형태로 리턴된다. 보통은 미리 구조 분해하여 선언과 동시에 값을 받을 수 있도록 해준다.

 

배열이나 객체 값을 state로 사용할 경우 state가 참조형 값 자체를 가지고 있는 게 아니라 그 배열이나 객체의 주솟값을 참조하고 있는 것이다. 때문에 단순히 객체 내부의 값을 변경하는 것만으로는 리액트가 state의 상태가 변경되지 않았다(참조하는 객체의 주솟값은 같으므로)고 판단하고, 화면을 랜더링하지 않는다. 따라서 참조형 state를 활용할 때는 (객체나 배열이나 상관 없이) 반드시 새로운 참조형 값을 만들어 state를 변경해야 한다.

import { useState } from 'react';

const random = (number) => randomNumber

const [recode, setRecode] = useState([]);
const result = random(6)

setRecode([...recode, result])

state를 이용하면 props의 값을 동적으로 변경할 수 있기 때문에 훨씬 스바라시한 컴포넌트를 만들 수 있다. 아래의 예시를 살펴보자.

const [num, setNum] = useState(1)

const handleButtonClick = () => {
  const getRandomNumber = () => {
    min = Math.ceil(min);
    max = Math.floor(max);
    return Math.floor(Math.random() * (max - min + 1)) + min;
  }
    setNum(getRandomNumber())
}

<Button onClick={handleButtonClick}/>

<Dice color="blue" num={num} />

버튼을 누르면 복잡(?)한 함수를 통해 1부터 6까지의 임의의 숫자 하나가 setNum을 통해 num state에 저장된다. Dice 컴포넌트는 이 num 값을 받아 src를 정하기 때문에, 결과적으로 버튼을 눌러서 Dice의 이미지를 랜덤하게 바꿔줄 수 있게 되는 것이다.

 

 

배열을 컴포넌트로 출력하기

앞서 나는 리액트 컴포넌트를 설명하면서 이를 사용함으로써 '리액트 엘리먼트의 재사용성을 높이고, 다양한 속성들을 자유롭게 사용'할 수 있게 된다고 말했다. 아래의 메소드를 이용하면 배열에 데이터를 담아두고, 이를 리액트 컴포넌트를 통해 재사용성을 극한으로 높일 수 있다.

 

map()으로 배열 랜더링

class SocialMedia {
  constructor(url, title, img) {
    this.url = url;
    this.title = title;
    this.img = img;
  }
}

const socialMediaArray = [
  new SocialMedia('https://ko-kr.facebook.com', 'facebook', facebook),
  new SocialMedia('https://twitter.com', 'twitter', twitter),
  new SocialMedia('https://www.instagram.com', 'instagram', instagram),
  new SocialMedia('https://www.youtube.com', 'youtube', youtube),
];

<div>
  {socialMediaArray.map((media) => {
    return (
      <a
        key={media.title}
        href={media.url}
        target="_blank"
        rel="noopener noreferrer"
        title={media.title}
      >
        <img src={media.img} alt={media.title} />
      </a>
    );
  })}
</div>

(아마도) 정돈된 데이터를 일괄적으로 보여주는 데 가장 용이한 방식이라고 생각된다. map 메소드의 콜백 함수로 리액트 컴포넌트를 리턴하면 배열의 길이만큼 반복하여 서로 다른 컴포넌트를 출력한다. 이러한 콜백 함수 내에서 사용된 state의 경우 다른 컴포넌트와 구별되는 고유한 state 값으로 사용이 가능하다.

 

sort()로 배열 순서 변경

import { useState } from 'react';
import ReviewList from './ReviewList';
import items from '../mock.json';

function App() {
  const [order, setOrder] = useState('createdAt');
  const sortedItems = items.sort((a, b) => b[order] - a[order]);

  const handleNewestClick = () => setOrder('createdAt');
  const handleBestClick = () => setOrder('rating');

  return (
    <div>
      <div>
        <button onClick={handleNewestClick}>최신순</button>
        <button onClick={handleBestClick}>베스트순</button>
      </div>
      <ReviewList items={sortedItems} />
    </div>
  );
}

export default App;

버튼을 클릭하면 setOrder()를 통해 order state의 값이 달라진다. 앞서 살펴본 것처럼 state값이 바뀌었기 때문에 화면이 다시 랜더링된다. 이 때, 코드를 다시 파씽하여 Virtual DOM을 생성하는 과정에서 sortedItems에는 sort 메소드를 통해 순서가 변경된 item 배열이 전달되게 된다.

이렇게 변경된 item은 ReviewList 컴포넌트의 어딘가에서 map 메소드를 통해 각각의 컴포넌트로 출력되겠지?

 

filter()로 배열 아이템 삭제

const [items, setItems] = useState(mockItems);

const handleDelete = (id) => {
  const nextItems = items.filter((item) => item.id !== id);
  setItems(nextItems);
};

// ...
<ReviewList items={sortedItems} onDelete={handleDelete} />
function ReviewListItem({ item, onDelete }) {
  const handleDeleteClick = () => {
    onDelete(item.id);
  };

  return (
    <div>
      <img src={item.imgUrl} alt={item.title} />
      <div>
        <h1>{item.title}</h1>
        <p>{formatDate(item.createdAt)}</p>
        <button onClick={handleDeleteClick}>삭제</button>
      </div>
    </div>
  );
}

handleDelete 함수는 특정한 id를 받아서, 이 id를 제외한 새로운 배열을 setItems를 통해 state에 반영하고 있다. 이렇게만 이야기해놓으면 별 문제 없는 것 같지만, 잘 생각해보면 어딘가 앞뒤가 맞지 않는 부분이 존재한다는 사실을 알 수 있다.

특정한 id를 받기 위해서는 그 id가 드러나는 ReviewListItem 컴포넌트 수준까지 내려가야 한다. 그런데 handleDelete를 ReviewListItem 컴포넌트에서 구현하자니 또 state 관리에 문제가 생긴다. 이를 해결하기 위해서는 하나의 기능을 위해 서로 다른 두 곳에서 함수의 작동이 이루어져야 하는 것이다.

그러한 까닭에 handleDelete 함수에서 대부분의 로직이 완성되었지만, 특정한 id를 받아내기 위해서 하위 컴포넌트까지 내려가 handleDeleteClick 함수에서 호출되는 것이다.

 

이러한 디자인 패턴(?)을 깊이 생각해보자.

블로그의 정보

Ayden's journal

Beard Weard Ayden

활동하기