반복되는 컴포넌트 처리하기

박지현·2023년 2월 13일

React 입문

목록 보기
12/12
post-thumbnail

2023.02.06

리액트에서의 map

우리 코드 다시 확인해보기

지난시간 아래와 같은 화면을 구현했었다.

import React from "react";

const App = () => {
  const style = {
    padding: "100px",
    display: "flex",
    gap: "12px",
  };

  const squareStyle = {
    width: "100px",
    height: "100px",
    border: "1px solid green",
    borderRadius: "10px",
		display: "flex",
    alignItems: "center",
    justifyContent: "center",
  };

  return (
    <div style={style}>
      <div style={squareStyle}>감자</div>
      <div style={squareStyle}>고구마</div>
      <div style={squareStyle}>오이</div>
      <div style={squareStyle}>가지</div>
      <div style={squareStyle}>옥수수</div>
    </div>
  );
};

export default App;

하지만 이렇게 하는 것은 같은 코드가 여러 번 중복이된다. “감자", “고구마" 와 같은 이름들도 개발자가 직접 입력을 해줘야 하고, 데이터로서 관리가 되고 있지 않다. 만약에 “토마토" 라는 항목이 추가되면 개발자는 화면을 추가로 개발해줘야 한다.

map을 이용해서 구현해보기

바로 map()(자바스크립트 메서드)을 사용하는 것 이다.

map() 을 사용하기 위해 채소들의 이름을 배열로 만들어보자.

// src/App.js

function App(){
	// .. 중략
	const vegetables = ["감자", "고구마", "오이", "가지", "옥수수"];

	return <div></div>
}

// .. 중략

그리고 JSX 부분에서 아래와 같이 작성한다.

import React from "react";

  const vegetables = ["감자", "고구마", "오이", "가지", "옥수수"];

  return (
    <div className="app-style">
      {vegetables.map((vegetableName) => {
        return (
          <div className="square-style" key={vegetableName}>
            {vegetableName}
          </div>
        );
      })}
    </div>
  );
};

export default App;

JSX 부분에서 map() 즉, 자바스크립트 코드를 작성할 것 이기때문에 { } 로 먼저 감싸고 시작한다.
JSX에서 map() 은 배열의 모든 요소를 순회한다. 그래서 클라이언트에서는 배열 형태의 데이터를 활용해서 화면을 그려주는 경우가 많고, 이때 배열의 값들로 동적으로 컴포넌트를 만들수 있다.
map을 사용하니 중복된 코드가 사라지고 1개의 컴포넌트를 이용하면서 그 안에서 <div>{vegetableName}</div> 가 순차적으로 보여지고 있다.

복잡한 데이터 다뤄보기

(1) 객체가 담긴 배열 다뤄보기

조금 더 복합한 구조의 데이터를 다뤄보자. 주어진 정보는 다음과 같다. user 라는 정보이며 배열안에 object literal 형태의 데이터가 있다.

const users = [
    { id: 1, age: 30, name: "송중기" },
    { id: 2, age: 24, name: "송강" },
    { id: 3, age: 21, name: "김유정" },
    { id: 4, age: 29, name: "구교환" },
  ];

App 컴포넌트에서는 user.map() 을 통해 user의 정보를 순회하고 각각의 user 정보를 User 컴포넌트로 주입해줍니다.

import React from 'react';
import './App.css'; // 🔥 반드시 App.css 파일을 import 해줘야 합니다.

//  User 컴포넌트를 분리해서 구현
function User(props) {
  return (
    <div>{props.user.age}살 - {props.user.name}</div>
  );
}

const App = () => {
  const users = [
    { id: 1, age: 30, name: '송중기' },
    { id: 2, age: 24, name: '송강' },
    { id: 3, age: 21, name: '김유정' },
    { id: 4, age: 29, name: '구교환' },
  ];
  return (
    <div className="app-container">
      {users.map((user) => {
        return <User user={user} key={user.id} />;
      })}
    </div>
  );
};

export default App;

User 컴포넌트에 props로 들어오는 user의 정보는 { id: 1, age: 30, name: "송중기" } 라는 정보가 들어오는 것이다.
map() 의 기능을 이용해서 우리는 앞으로 반복되는 컴포넌트를 간단하게 화면에 표시할 수 있다.

(2) state 복습

리액트에서 동적으로 변화는 데이터는 state라는 상태 값으로 관리한다. 기존 정적 배열 데이터로 관리했던 유저 리스트를 useState를 활용해 변화가 일어나면 리랜더링을 초래하는 ‘상태 값’으로 만들어보자.

//기존 users 배열
const users = [
    { id: 1, age: 30, name: '송중기' },
    { id: 2, age: 24, name: '송강' },
    { id: 3, age: 21, name: '김유정' },
    { id: 4, age: 29, name: '구교환' },
  ];
  
//useState를 이용한 상태값 만들기
const [users, setUsers] = useState([
    { id: 1, age: 30, name: '송중기' },
    { id: 2, age: 24, name: '송강' },
    { id: 3, age: 21, name: '김유정' },
    { id: 4, age: 29, name: '구교환' },
  ]);

(3) 추가/삭제를 위한 준비

여기에서 user를 추가하고, 삭제하는 기능을 추가할 것이다. 그것을 위한 UI를 작성한다.

import React from 'react';
import './App.css'; // 🔥 반드시 App.css 파일을 import 해줘야 합니다.

//  User 컴포넌트를 분리해서 구현
function User(props) {
  return (
    <div>{props.user.age}살 - {props.user.name}</div>
  );
}

const App = () => {
  const [users, setUsers] = useState([
    { id: 1, age: 30, name: '송중기' },
    { id: 2, age: 24, name: '송강' },
    { id: 3, age: 21, name: '김유정' },
    { id: 4, age: 29, name: '구교환' },
  ]);
  
  const [name, setName] = useState(''); // <-- 유저의 입력값을 담을 상태
  const [age, setAge] = useState('');

  return (
    <div className="app-container">
      <input value={name}
        placeholder="이름을 입력해주세요"
			// 인풋 이벤트로 들어온 입력 값을 name의 값으로 업데이트
        onChange={(e) => setName(e.target.value)} 
      />
			<input value={age}
        placeholder="나이를 입력해주세요"
			// 인풋 이벤트로 들어온 입력 값을 age의 값으로 업데이트
        onChange={(e) => setName(e.target.value)} 
      />
      {users.map((user) => {
        return <User user={user} key={user.id} />;
      })}
    </div>
  );
};

export default App;

(4) user 추가

import React from 'react';
import './App.css'; // 🔥 반드시 App.css 파일을 import 해줘야 합니다.

//  User 컴포넌트를 분리해서 구현
function User(props) {
  return (
    <div className="user-card">
      <div>{props.user.age}살 - </div>
      <div>{props.user.name}</div>
    </div>
  );
}

const App = () => {
  const [users, setUsers] = useState([
    { id: 1, age: 30, name: '송중기' },
    { id: 2, age: 24, name: '송강' },
    { id: 3, age: 21, name: '김유정' },
    { id: 4, age: 29, name: '구교환' },
  ]);
  const [name, setName] = useState(''); // <-- 유저의 입력값을 담을 상태
  const addUserHandler = () => {        // -- 유저추가 핸들러 기능
    const newUser = {
      id: users.length + 1,             // -- 좋은 코드는 아니므로 변경예정
      age: age,
      name: name,
    };

    setUsers([...users, newUser]);
  };
  return (
    <div className="app-container">
      <input
        value={name}
			// 인풋 이벤트로 들어온 입력 값을 name의 값으로 업데이트
        onChange={(e) => setName(e.target.value)} 
      />
      {users.map((user) => {
        return <User user={user} key={user.id} />;
      })}
      <button onClick={addUserHandler}>추가하기</button>
    </div>
  );
};

export default App;

(5) user 삭제

import React from 'react';
import './App.css'; // 🔥 반드시 App.css 파일을 import 해줘야 합니다.

//  User 컴포넌트를 분리해서 구현
function User(props) {
  return (
    <div className="user-card">
      <div>{props.user.age}살 - </div>
      <div>{props.user.name}</div>
      <button onClick={() => props.handleDelete(props.user.id)}>
        삭제하기
      </button>
    </div>
  );
}

const App = () => {
  const [users, setUsers] = useState([
    { id: 1, age: 30, name: '송중기' },
    { id: 2, age: 24, name: '송강' },
    { id: 3, age: 21, name: '김유정' },
    { id: 4, age: 29, name: '구교환' },
  ]);
  const [name, setName] = useState(''); // <-- 유저의 입력값을 담을 상태
  const addUserHandler = () => {
    const newUser = {
      id: users.length + 1,
      age: 30,
      name: name,
    };

    setUsers([...users, newUser]);
  };
	const deleteUserHandler = (id) => {
    const newUserList = users.filter((user) => user.id !== id);
    setUsers(newUserList);      // filter를 이용해 예외를 지정후 다시돌려줌
  };
  return (
    <div className="app-container">
      <input
        placeholder="이름을 입력해주세요"
        value={name}
			// 인풋 이벤트로 들어온 입력 값을 name의 값으로 업데이트
        onChange={(e) => setName(e.target.value)} 
      />
      {users.map((user) => {
        return <User user={user} key={user.id} handleDelete={deleteUserHandler}/>;
      })}
      <button onClick={addUserHandler}>추가하기</button>
    </div>
  );
};

export default App;

(6) 컴포넌트 분리

이런식으로 버튼과 기능들이 많이지다보면 복잡해지고, 재사용하기가 힘들다. 이러한 부분들을 재사용하기위해 버튼을 컴포넌트로 분리한다.

import React from 'react';
import './App.css'; // 🔥 반드시 App.css 파일을 import 해줘야 합니다.

//1. 버튼 컴포넌트 생성
function CustomButton(props) {
  return <button onClick={props.onClick}>{props.children}</button>;
}


//  User 컴포넌트를 분리해서 구현
function User(props) {
  return (
    <div className="user-card">
      <div>{props.user.age}살 - </div>
      <div>{props.user.name}</div>
			//2. 버튼을 컴포넌트로 바꾸기
      <CustomButton onClick={() => props.handleDelete(props.user.id)}>
        삭제하기
      </CustomButton>
    </div>
  );
}

const App = () => {
  const [users, setUsers] = useState([
    { id: 1, age: 30, name: '송중기' },
    { id: 2, age: 24, name: '송강' },
    { id: 3, age: 21, name: '김유정' },
    { id: 4, age: 29, name: '구교환' },
  ]);
  const [name, setName] = useState(''); // <-- 유저의 입력값을 담을 상태
  const addUserHandler = () => {
    const newUser = {
      id: users.length + 1,
      age: 30,
      name: name,
    };

    setUsers([...users, newUser]);
  };
	const deleteUserHandler = (id) => {
    const newUserList = users.filter((user) => user.id !== id);
    setUsers(newUserList);
  };
  return (
    <div className="app-container">
      <input
        placeholder="이름을 입력해주세요"
        value={name}
			// 인풋 이벤트로 들어온 입력 값을 name의 값으로 업데이트
        onChange={(e) => setName(e.target.value)} 
      />
      {users.map((user) => {
        return <User user={user} key={user.id} handleDelete={deleteUserHandler}/>;
      })}
			//3. 버튼을 컴포넌트로 바꾸기
      <CustomButton onClick={addUserHandler}>추가하기</CustomButton>
    </div>
  );
};

export default App;

(7) 버튼 색상 변경하기

이렇게 컴포넌트를 따로 분리해주면, 버튼 컴포넌트를 호출할 때 색상에 대한 정보를 props로 전달함으로써 추가하기 버튼은 초록색으로 삭제하기 버튼은 빨강색으로 쉽게 변경할수 있습니다.

//  Button 컴포넌트 부분만
function CustomButton(props) {
const {color, onClick, children} = props

  if (color)
    return (
      <button
        style={{ background: color, color: "white" }}
        onClick={onClick}
      >
        {children}
      </button>
    );

  return <button onClick={onClick}>{props.children}</button>;
}
<Button color="red" onClick={() => props.handleDelete(props.user.id)}>삭제하기</Button>
<Button color="green" onClick={handleAdd}>추가하기</Button>

(8) 독립 컴포넌트

현재 App컴포넌트와 User컴포넌트, Button컴포넌트가 모두 App.js라는 파일 한 곳에 작성되어 있기 때문에 발생하는 몇 가지 문제들이 있다.

  1. App.js 파일의 역할이 명확하지 않다.
  2. 컴포넌트 분리를 통해 가독성을 높였지만, 두 컴포넌트의 사이즈가 커지거나 혹은 또 다른 컴포넌트를 작성하게 된다면 가독성은 금방 떨어지게 될 것이다.
  3. 현재 프로젝트 구조에서, User 컴포넌트, Button 컴포넌트가 어디에 작성되어 있는지 찾기가 힘들다. 특히 작성자가 아닌 다른 개발자가 App.js 파일을 보고 User 컴포넌트, Button 컴포넌트가 해당 파일에 작성되어 있다고 유추하기 쉽지 않다.

때문에 기능을 재사용 하는 컴포넌트들을 컴포넌트끼리 폴더로 묶어서 따로 관리해서 사용하는것이 좋다.

(예시 이미지)

버튼 컴포넌트를 따로 독립시켜주었다.

// 경로: src/components/Button.js

function Button(props) {
  switch (props.color) {
    case 'green': {
      return (
        <button
          style={{ background: 'green', color: 'white' }}
          onClick={props.onClick}
        >
          {props.children}
        </button>
      );
    }
    case 'red': {
      return (
        <button
          style={{ background: 'red', color: 'white' }}
          onClick={props.onClick}
        >
          {props.children}
        </button>
      );
    }
    default: {
      return <button onClick={props.onClick}>{props.children}</button>;
    }
  }
}

export default Button;

// 💡💡 외부 모듈(파일)에서 Sqaure 컴포넌트를 사용할 수 있게 export(내보내기)해줘야 한다.

이후는 기존의 App.js

// 경로: src/App.js

import React from 'react';
import Button from './components/Button.js';

//  User 컴포넌트를 분리해서 구현
function User(props) {
  return (
    <div className="user-card">
      <div>{props.user.age}살 - </div>
      <div>{props.user.name}</div>
			//2. 버튼을 컴포넌트로 바꾸기
      <Button onClick={() => props.handleDelete(props.user.id)}>
        삭제하기
      </Button>
    </div>
  );
}

const App = () => {
  const [users, setUsers] = useState([
    { id: 1, age: 30, name: '송중기' },
    { id: 2, age: 24, name: '송강' },
    { id: 3, age: 21, name: '김유정' },
    { id: 4, age: 29, name: '구교환' },
  ]);
  const [name, setName] = useState(''); // <-- 유저의 입력값을 담을 상태
  const addUserHandler = () => {
    const newUser = {
      id: users.length + 1,
      age: 30,
      name: name,
    };

    setUsers([...users, newUser]);
  };
	const deleteUserHandler = (id) => {
    const newUserList = users.filter((user) => user.id !== id);
    setUsers(newUserList);
  };
  return (
    <div className="app-container">
      <input
        placeholder="이름을 입력해주세요"
        value={name}
			// 인풋 이벤트로 들어온 입력 값을 name의 값으로 업데이트
        onChange={(e) => setName(e.target.value)} 
      />
      {users.map((user) => {
        return <User user={user} key={user.id} handleDelete={deleteUserHandler}/>;
      })}
			//3. 버튼을 컴포넌트로 바꾸기
      <Button onClick={addUserHandler}>추가하기</Button>
    </div>
  );
};

export default App;

key란?

(1) 혹시 브라우저 콘솔에서 에러가 뜬다면?

리액트에서 map을 사용하여 컴포넌트를 반복 렌더링 할 때는 반드시 컴포넌트에 key를 넣어줘야 한다. key가 필요한 이유는 React에서 컴포넌트 배열을 렌더링했을 때 각각의 원소에서 변동이 있는지 알아내려고 사용하기 때문이다. 만약 key가 없다면 React는 가상돔을 비교하는 과정에서 배열을 순차적으로 비교하면서 변화를 감지하려 하지만, key가 있으면 이 값을 이용해서 어떤 변화가 일어났는지 더 빠르게 알아낼 수 있게 된다.
즉, key값을 넣어줘야 React의 성능이 더 최적화 된다.

(2) key를 넣는 방법

key는 아래와 같이 넣어준다.

  <div style={style}>
      {users.map((user) => {
        return <User user={user} key={user.id} />;
      })}
    </div>

간혹 map((value, index)=>{}) 처럼, map에서 지원해주는 index를 사용해서 key를 넣는 경우가 있는데, 이것은 좋지 않은 방식이지 최대한 지양해주자.

profile
프론트엔드가 목표!

0개의 댓글