React 컴포넌트 반복

Yubin·2022년 1월 18일
0

웹 어플리케이션을 만들다 보면 다음과 같이 반복되는 코드를 작성할 때가 있다. src 디렉터리에 IterationSample.js라는 파일을 작성해서 다음 코드를 적어보자.

IterationSample.js

const IterationSample = () => {
    return (
        <ul>
            <li>사과</li>
            <li>파인애플</li>
            <li>멜론</li>
            <li>바나나</li>
        </ul>
    )
}

export default IterationSample;

코드에서 <li> ... </li>태그가 반복되는 것을 볼 수 있다.

지금은 li 태그 하나뿐이라 그렇게 문제가 되지는 않을 것 같다. 하지만 코드가 좀 더 복잡하다면 어떨까? 코드양은 더더욱 늘어날 것이며, 파일 용량도 쓸데없이 증가할거다. 이는 낭비이다. 또 보여 주어야 할 데이터가 유동적이라면 이런 코드로는 절대로 관리하지 못한다.

이번 글에서는 리액트 프로젝트에서 반복적인 내용을 효율적으로 보여 주고 관리하는 방법을 알아보겠다.

자바스크립트 배열의 map() 함수

자바스크립트 배열 객체의 내장 함수인 map 함수를 사용하여 반복되는 컴포넌트를 렌더링할 수 있다. map함수는 파라미터로 전달된 함수를 사용해서 배열 내 각 요소를 원하는 규칙에 따라 변환한 후 그 결과로 새로운 배열은 생성한다.

문법

arr.map(callback, [thisArg])

이 함수의 파라미터는 다음과 같다.

  • callback : 새로운 배열의 요소를 생산하는 함수로 파라미터는 다음 세 가지 이다.
    -currentValue : 현제 처리하고 있는 요소
    -index : 현제 처리하고 있는 요소의 index 값
    -array : 현제 처리하고 있는 원본 배열
  • thisArg(선택 항목) : callback 함수 내부에서 사용할 this 레퍼런스

예제

map 함수를 사용하여 배열 [1,2,3,4,5]의 각 요소를 제곱해서 새로운 배열을 생성하겠다.

var numbers = [1, 2, 3, 4, 5];
    var processed = numbers.map(function (num) {
        return num * num
    })
    console.log(processed)

결과 화면

이처럼 map 함수는 기존 배열로 새로운 배열을 만드는 역할을 한다. 이 코드를 ES6문법으로 작성 한다면

const numbers = [1, 2, 3, 4, 5];
    const processed = numbers.map((num) => num * num)
    console.log(processed)

이런식으로 작성할 수 있을거같다. var키워드 대신 const를 사용했고, 에로우 펑션을 사용했다.

데이터 배열을 컴포넌트 배열로 변환하기

아까는 기존 배열에 있는 값들을 제곱하여 새로운 배열을 생성했다. 똑같은 원리로 기존 배열을 컴포넌트로 구성된 배열을 생성할 수도 있다.

컴포넌트 수정

아까 만들었떤 IterationSample.js 컴포넌트를 다음과 같이 수정해보자.

IterationSample.js

const IterationSample = () => {
    const fruits = ['사과', '파인애플', '멜론', '바나나', '수박'];
    const fruitList = fruits.map((fruit) => <li>{fruit}</li>)
    return (
        <div>
            <ul>
                {fruitList}
            </ul>
        </div>
    )
}

export default IterationSample;

문자열로 구성된 배열을 선언한다. 그 배열 값을 사용해 <li>...</li> JSX 코드로 된 배열을 새로 생성한 후 fruitList에 담는다.

map 함수에서 JSX를 작성할 떄는 앞서 다룬 예제처럼 DOM 요소를 작성해도 되고, 컴포넌트를 사용해도 된다.

결과 화면

원하는 대로 렌더링이 된거같다. 하지만 아직 완벽하지 않다. 크롬 개발자 도구의 콘솔을 열면 "key" prop이 없다고 경고 메시지를 표시한다. key란 무엇일까?

key

리액트에서 key는 컴포넌트 배열을 렌더링했을 때 어떤 원소에 변동이 있었는지 알아내려고 사용한다. 예를 들어 유동적인 데이터를 다룰 때는 원소를 새로 생성할 수도, 제거할 수도, 수정할 수도 있다. key가 없을 때는 Virtual DOM을 비교하는 과정에서 리스트를 순차적으로 비교하면서 변화를 감지한다. 하지만 key가 있다면 이 값을 사용해 어떤 변화가 일어났는지 더욱 빠르게 알 수 있다.

key 설정

key값을 설정할 때는 map함수의 인자로 전달되는 함수 내부에서 컴포넌트 props를 설정하듯이 설장하면 된다. key값은 언제나 유일해야 한다. 따라서 데이터가 가진 고윳값을 key값으로 설정해야 한다. 예를 들어 다음과 같이 게시판의 게시물을 렌더링한다면 게시물 번호를 key 값으로 설정해야 한다.

const articleList = articles.map(article) => {
    <Article
        title={article.title}
        wrier={article.wrier}
        key={article.key}
    />

하지만 앞서 만들었던 예제 컴포넌트에는 이런 고유 번호가 없다. 이때는 map함수에 전달되는 콜백 함수의 인수인 index값을 사용하면 된다.

IterationSample.js

const IterationSample = () => {
    const fruits = ['사과', '파인애플', '멜론', '바나나', '수박'];
    const fruitList = fruits.map((fruit, index) => <li key={index}>{fruit}</li>)

    return (
        <div>
            <ul>
                {fruitList}
            </ul>
        </div>
    )
}

export default IterationSample;

이렇게 코드를 수정하면 개발자 도구에서 더 이상 경고 메시지를 표시하지 않는다. 고유한 값이 없을 때만 index값을 key로 사용해야 한다. index를 key로 사용하면 배열이 변경될 때 효율적으로 리렌더링하지 못한다. index를 사용하는 경우는 데이터가 완전히 바뀌지 않는 경우에 한에 쓸 수 있다는거다.

응용

지금까지 배운 개념을 응용하여 고정된 배열을 렌더링하는 것이 아닌, 동적인 배열을 렌더링하는 것을 구현해 보겠다. 그리고 index값을 key로 사용하면 리렌더링이 비효율적이라고 배웠는데, 이러한 상황에 어떻게 고윳값을 만들 수 있는지도 알아보겠다.

초기 상태 설정

IterationSample 컴포넌트에서 useState를 사용하여 상태를 설정하겠다. 세 가지 상태를 사용할 텐데 하나는 데이터 배열이고, 다른 하나는 텍스트를 입력할 수 있는 input의 상태이다.
그럼 마지막 하나는 무엇일까? 그것은 데이터 배열에서 새로운 항목을 추가할 때 사용할 고유 id를 위한 상태이다.

배열을 설정할 때. 조금 전에는 단순히 문자열로 이루어진 배열을 만들었지만, 이번에는 객체 형태로 이루어진 배열을 만들겠다. 해당 객체에는 문자열과 고유 id 값이 있다.

IterationSample.js

import { useState } from "react";

const IterationSample = () => {
    const [fruits, setFruits] = useState([
        { id: 1, text: '사과' },
        { id: 2, text: '파인애플' },
        { id: 3, text: '멜론' },
        { id: 4, text: '바나나' },
        { id: 5, text: '수박' }
    ]);
    const [inputText, setinputText] = useState('');
    const [nextId, setNextId] = useState(5); // 새로운 항목을 추가할 떄 사용할 id
    const fruitList = fruits.map(fruit => <li key={fruit.id}>{fruit.text}</li>)

    return (
        <div>
            <ul>
                {fruitList}
            </ul>
        </div>
    )
}

export default IterationSample;

이번에는 map함수를 사용할 때 key값을 index대신 name.id 값으로 지정해 주었다.

데이터 추가 기능 구현

이제 새로운 이름을 등록할 수 있는 기능을 구현해 보겠다.
우선 ui 태그의 상단에 input와 button을 렌더링하고, input의 상태를 관리해보자.

IterationSample.js

import { useState } from "react";

const IterationSample = () => {
    const [fruits, setFruits] = useState([
        { id: 1, text: '사과' },
        { id: 2, text: '파인애플' },
        { id: 3, text: '멜론' },
        { id: 4, text: '바나나' },
        { id: 5, text: '수박' }
    ]);
    const [inputText, setinputText] = useState('');
    const [nextId, setNextId] = useState(5); // 새로운 항목을 추가할 떄 사용할 id
    const fruitList = fruits.map(fruit => <li key={fruit.id}>{fruit.text}</li>)
    const onChange = (e) => setinputText(e.target.value);
    return (
        <div>
            <ul>
                <input
                    type="text"
                    value={inputText}
                    onChange={onChange}
                />
                <button>추가</button>
                {fruitList}
            </ul>
        </div>
    )
}

export default IterationSample;

그 다음은 버튼을 클릭했을 때 호출할 onClick 함수를 선언하여 버튼의 onClick 이벤트를 설정해 보겠다.

onClick 함수에서는 배열의 내장 함수 concat를 사용해 새로운 항목을 추가한 배열을 만들고, setNames를 통해 상태를 업데이트 해준다.

import { useState } from "react";

const IterationSample = () => {
    const [fruits, setFruits] = useState([
        { id: 1, text: '사과' },
        { id: 2, text: '파인애플' },
        { id: 3, text: '멜론' },
        { id: 4, text: '바나나' },
        { id: 5, text: '수박' }
    ]);
    const [inputText, setInputText] = useState('');
    const [nextId, setNextId] = useState(6); // 새로운 항목을 추가할 떄 사용할 id
    const fruitList = fruits.map(fruit => <li key={fruit.id}>{fruit.text}</li>)
    const onChange = e => setInputText(e.target.value);
    const onClick = () => {
        const nextFruit = fruits.concat({
            id: nextId,
            text: inputText,
        });
        setNextId(nextId + 1); // nextId값에 1 +
        setFruits(nextFruit); // fruit값 업데이트
        setInputText(''); // inpuText값 초기화
    }
    return (
        <div>
            <ul>
                <input
                    value={inputText}
                    onChange={onChange}
                />
                <button onClick={onClick}>추가</button>
                {fruitList}
            </ul>
        </div>
    )
}

export default IterationSample;

배열에 새 항목을 추가할 때 배열의 push 함수를 사용하지않고 concat을 사용했다. push 함수는 기존 배열 자체를 변경해 주는 반면, concat은 새로운 배열을 만들어 준다는 차이가 있다.

리액트에서 상태를 업테이트할 때는 기존 상태를 그대로 두면서 새로운 값을 상태로 설정해야 한다. 이를 불변성 유지라고 하는데, 불변성 유지를 해 주어야 나중에 리액트 컴포넌트의 성능을 최적화할 수 있다.

onClick 함수에서 새로운 항목을 추가할 때 객체의 id 값은 nextId를 사용하도록 하고, 클릭될 때 마다 값이 1씩 올라가도록 구현했다. 추가로 버튼이 클릭될 때 기존의 input 내용을 비우는것도 구현했다.

하지만 지금은 원래 id가 5밖에 없지만 늘어날때마다 nextId 값을 바꿔줘야한다. 번거로움을 방지하기 위해 코드를 다시 작성해 봤다.

IterationSample.js

const IterationSample = () => {
    const [foods, setFoods] = useState([
        { id: 1, name: '삽겹살' },
        { id: 2, name: '피자' },
        { id: 3, name: '햄버거' },
        { id: 4, name: '치킨' },
        { id: 5, name: '초밥' },
        { id: 6, name: '김치찌개' },
    ]);
    const ID갯수 = foods.length + 1;
    const foodList = foods.map((food) => <li key={food.id}>{food.name}</li>);
    const [nextId, setNextId] = useState(ID갯수);
    const [textInput, setTextInput] = useState('');
    const onChange = (e) => setTextInput(e.target.value)
    const onClick = () => {
        const nextFood = foods.concat({
            id: nextId,
            name: textInput
        })
        setTextInput('');
        setNextId(nextId + 1)
        setFoods(nextFood);
    }

이렇게 구현하니 문제없이 잘 작동된다.

데이터 제거 기능 구현

이번에는 각 항목을 더블클릭했을 때 해당 항목이 화면에서 사라지는 기능을 구현해 보겠다. 이번에도 마찬가지로 불변성을 유지하면서 업데이트해 주어야 한다. 불변성을 유지하면서 배열의 특정 항목을 지울 때는 배열의 내장 함수 filter를 사용하면 된다.

filter 함수를 사용하면 배열에서 특정 조건으로 만족하는 원소들만 쉽게 분류할 수 있다. 사용 예시를 한번 보자.

const numbers = [1,2,3,4,5,6];
const biggerThanThree = numbers.filter(number => number > 3);
//결과: [4,5,6]

filter 함수의 인자에 분류하고 싶은 조건을 반환하는 함수를 넣어 주면 쉽게 분류할 수 있다.
이 filter 함수를 응용하여 특정 배열에서 특정 원소만 제외시킬 수도 있다. 예를 들어 위 코드에서 본 numbers 배열에서 3만 없애고 싶다면 다음과 같이 하면 된다.

const numbers = [1,2,3,4,5,6];
const withoutThree = numbers.filter(number => number !== 3);
//결과: [1,2,4,5,6]

이제 filter 함수를 사용하여 IterationSample 컴포넌트의 항목 제거 기능을 구현해 보자.
onRemove라는 함수를 만들고 버튼을 추가해 이벤트 등록을 해주자.

IterationSample.js

import { useState } from "react";

const IterationSample = () => {
    const [foods, setFoods] = useState([
        { id: 1, name: '삽겹살' },
        { id: 2, name: '피자' },
        { id: 3, name: '햄버거' },
        { id: 4, name: '치킨' },
        { id: 5, name: '초밥', },
        { id: 6, name: '김치찌개' },
    ]);
    const ID갯수 = foods.length + 1;
    const onRemove = id => {
        const nextFood = foods.filter(food => food.id !== id);
        setFoods(nextFood)
    }
    const foodList = foods.map((food) =>
        <li key={food.id}>{food.name} <button onClick={() => onRemove(food.id)}>삭제</button></li>
    );
    const [nextId, setNextId] = useState(ID갯수);
    const [textInput, setTextInput] = useState('');
    const onChange = (e) => setTextInput(e.target.value)
    const onClick = () => {
        const nextFood = foods.concat({
            id: nextId,
            name: textInput,
        })
        setTextInput('');
        setNextId(nextId + 1)
        setFoods(nextFood);
    }
    return (
        <div>
            <input
                type="text"
                onChange={onChange}
                value={textInput}
            />
            <button onClick={onClick}>추가</button>
            <ul>
                {foodList}
            </ul>
        </div>
    )
}

export default IterationSample;

정리

이 글에수는 반복되는 데이터를 렌더링하는 방법을 배우고, 이를 응용하여 유동적인 배열을 다루어 보았다. 컴포넌트 배열을 렌더링할 때는 key값 설정에 항상 주의해야 한다. 또 key값은 언제나 유일해야한다. key값이 중복된다면 렌더링 과정에서 오류가 발생한다.

상태 안에서 배열을 변형할 때는 배열에 직접 접근하여 수정하는 것이 아니라 concat, filter 등의 배열 내장 함수를 사용하여 새로운 배열을 만든 후 이를 새로운 상태로 설정해 주어야 한다는 점을 명심하자.

profile
꾸준히 기록하는 개발자가 꿈인 고등학생

0개의 댓글