JavaScript/알고리즘

프로그래머스 배열 조각하기 JS ( ⭐️ map과 forEach의 차이점 - react에서의 사용을 중심으로 )

hihiha2 2023. 6. 30. 18:06

문제 설명 

정수 배열 arr와 query가 주어집니다. query를 순회하면서 다음 작업을 반복합니다.

  • 짝수 인덱스에서는 arr에서 query[i]번 인덱스를 제외하고 배열의 query[i]번 인덱스 뒷부분을 잘라서 버립니다.
  • 홀수 인덱스에서는 arr에서 query[i]번 인덱스는 제외하고 배열의 query[i]번 인덱스 앞부분을 잘라서 버립니다.

위 작업을 마친 후 남은 arr의 부분 배열을 return 하는 solution 함수를 완성해 주세요.

 

🙋‍♀️ 내 생각

query를 순회하면서 작업을 반복하는데 짝수/홀수일 경우가 나눠진다.

순회를 위해서 forEach를 사용하였고, 홀짝의 분기처리를 위해서 if문을 사용했다.

각각의 인덱스에 맞게 값을 잘라내기 위해서 splice()를 해주었다.

 

문제를 풀면서 고민했던 점은 map을 쓸것인지 forEach를 쓸것인지였다.

문제를 forEach를 이용해서 풀었는데, 나머지코드는 동일하게 하고 map으로 바꿔도 결과는 통과했다. 

이 문제를 푸는데에는 map을 쓰든 forEach를 쓰든 상관이 없지만, 평소 리액트를 통해 작업할때 이 2가지의 차이점이 궁금했었기때문에 이번 기회로 공부하고 넘어가야겠다고 생각했다.

 

 

 

✅  내 코드

function solution(arr, query) {
    let result=[...arr]
    
    query.forEach((v,i)=> {
      if(i%2===0) {
       result.splice(query[i]+1)
      } else {
       result.splice(0,query[i])
    }
    })
    return result
}

result라는 변수에 [...arr]를 통해서 arr를 복사하였다. (원본 arr를 훼손하지 않기 위해)

 

그런 다음 query를 순회하기 위해서 forEach를 사용하였다.

forEach문 안에서 if문을 통해 짝수/홀수일 경우를 나누었다.

 

만약 i가 짝수일 경우에는 arr의 앞부분만 남겨야하기 때문에, 뒷부분을 잘라주는 식을 만들었다.

splice()를 통해서 어디부터 어디까지 자를지 범위를 지정했다.

 result.splice(query[i]+1)을 통해서 query[i]+1부터 자르고 앞부분만 남겼다.

 

만약 i가 홀수일 경우에는 arr의 뒷부분만 남겨야하기 때문에, 앞부분을 자른다.

이 또한 splice()를 통해서 범위를 지정했다. 맨앞부터(0) query[i]-1번째 값까지를 잘라야하기때문에 총 잘라야하는 길이는 query[i]개이다. 따라서 result를 splice(0,query[i])를 통해서 0부터 시작해서 query[i]개만큼 자른다.

 

forEach문을 돌면서 반복적으로 수행하고 모두 끝난 다음, result를 반환한다.

 

 

🤔 map과 forEach의 차이점

문제를 풀면서 궁금했던 점은 이 코드를 map을 통해서 풀면 안되나..? 결과가 뭐가 다르지? 하는 것이었다.

이전에 프로젝트를 하면서도 반복문인데 어떤 부분에서는 map을 쓰고 어떤 부분에서는 forEach를 사용했는데 정확하게 어떤 이유로 그렇게 쓰는지가 궁금했다. 

 

✔️ 공통점:  forEach와 map은 배열의 각 요소를 순회하며 작업을 수행하는 데 사용되는 메서드

 

✔️ 차이점

1. 반환 값:

  • forEach: forEach 메서드는 반환값이 없다 ➡️ 작업을 수행하고 원본 배열을 직접 수정하는 데 주로 사용
  • map: map 메서드는 새로운 배열을 반환.각 요소에 대해 작업을 수행한 결과를 새로운 배열로 만들어 반환한다 ➡️ 원본 배열은 변경X

 

🔫 리액트에서의 활용법

반환값의 차이때문에 리액트에서의 사용법이 달라진다.

 

forEach는 반환값이 없기때문에 JSX의 return문안에서 사용할 수 없다. 만약 return문안에서 사용하기 위해서는 JSX문 밖에 따로 함수를 만들고 그 안에서 각각의 요소를 돌면서 push를 통해서 값을 수정해줘야한다.

따라서 JSX문안에서 순회하는 코드를 만들고 싶으면 웬만하면 map을 사용하도록 한다.

map은 새로운값을 반환하는 성질이 있기때문에 return문 안에서 바로 사용할 수 있다.

만약 새로운 배열을 반환할 필요가 없거나 JSX문에서 render할 필요가 없으면 forEach를 사용한다.

 

 

 

2. 사용 용도:

  • forEach: forEach는 주로 반복 작업을 수행하고자 할 때 사용된다.

      예를 들면, 배열의 각 요소에 대해 특정 작업을 수행하거나, 값을 출력하거나, 외부 상태를 변경하는 등의 작업을 수행할 때 유용

  • map: map은 각 요소를 변환하여 새로운 배열을 생성하는 데 주로 사용된다. 각 요소에 대해 특정 작업을 수행하고, 그 결과를 새로운 배열의 요소로 매핑하여 반환

        따라서 원본 배열의 각 요소를 변환하여 새로운 배열을 만들고자 할 때 유용

 

 

 

🤔map과 forEach의 차이점중 가장 궁금했던 점이 바로 리액트에서의 사용용도였다.

map이 새로운 배열을 반환하고 싶을때 사용하는것은 알겠으나, 더 디테일하게 이러한 특징으로 인해서 또 어떤 차이점이 발생하고 그로인해서 사용용도도 달라지는 것을 구체적으로 알고싶었다. 

이러한 차이점을 가장 크게 느낀것은 firebase에서 데이터를 get메서드로 불러올때였다.

firebase의 공식문서에도 데이터를 추가할 경우 map이 아니라 forEach를 통해 구현하도록 설명되어있다. 어떤 원리때문에 map이 아니라 forEach를 쓰는지 궁금했다. 

firebase 공식문서

 

🔫 리액트에서의 활용법

새로운 배열을 만들지 않고 어떤 작업을 하고자 할때forEach를 사용, 새로운 배열을 만들고자할때 map을 사용한다.

 


forEach는 각 요소에 대해 특정작업을 수행하기 위해 사용된다.

 

🤔firebase를 통해서 백엔드를 구성하고 값을 받아와서 받아온 값을 통해서 serverData의 state를 업데이트하는 코드를 만들었다. 이때 파이어베이스에서 받아온 데이터를 사용할때 위의 공식문서처럼 forEach를 썼다. 공식문서에 그렇게 나와있기때문에 그렇게 쓰긴했었는데, map은 새로운 배열을 반환하고, forEach는 그렇지 않기때문에 메모리측면때문일까..?라고 생각했었다.

const [serverData, setServerData] = useState<ServerDataType[]>([]);

useEffect(() => {
    db.collection('post')
      .orderBy('timestamp', 'desc')
      .get()
      .then((querySnapshot) => {
        const list: ServerDataType[] = [];
        querySnapshot.forEach((doc) => {
          const { title, content } = doc.data();
          const { id } = doc;
          const serverData: ServerDataType = {
            title,
            content,
            id,
          };

          list.push(serverData);
        });
        setServerData(list);
      });
  }, []);

firebase의 'post'라는 이름의 collection에서 데이터를 받아오는 코드이다. 

 

리액트는 가상돔을 통해 상태가 변경된 것만을 인식하고 UI에 반영한다. 기존의 DOM과 변경된 DOM을 비교하여 변경된 부분만을 인식한다. 그래서 만약 원본을 변경한다면 상태변경이 정상적으로 작동하지 않을 수 있다. 그렇게 때문에 기존의 값을 직접변경하지 않고 새로운 값을 만들어서 그 값을 setServerData에 넣어야하는 것이다. 

 

그래서 list라는 빈배열을 만들고 forEach를 통해서 가져온 데이터중 필요한 값만을 추출하여 push를 통해서 setServerData를 통해서 값을 업데이트했다. list라는 빈비열을 굳이 만든 이유는 바로 불변성을 유지하기 위해서이다.

(불변성을 유지해야 가상돔이 정상적으로 state변화를 감지하고 UI에 업데이트하기 때문에)

forEach를 통해 추출한 값들만 모아서 list라는 배열에 push하고 setServerData에 list의 값을 넣는다. 그러면 리액트의 useState를 통해 serverData가 변경될 것이다. 

 

공식문서에 나와있어서 위와 같은 방식으로 만들었는데 그러면 map을 사용하면 안될까?하는 생각이 들었다. 왜 굳이 forEach를 사용해야하지? map을 사용하면 성능상의 문제가 있나? 그래서 리액트에서 map과 forEach의 사용용도에 대해 많이 찾아보고 어떤 이유때문일까 계속 생각했다. 그래서 위의 동일한 코드를 map으로 바꿔서 만들어보았다.

const [serverData, setServerData] = useState<ServerDataType[]>([]);

useEffect(() => {
    db.collection('post')
      .orderBy('timestamp', 'desc')
      .get()
      .then((querySnapshot) => {
        const updatedData = querySnapshot.docs.map((doc) => {
          const { title, content } = doc.data();
          const { id } = doc;
          const serverData: ServerDataType = {
            title,
            content,
            id,
          };
          return serverData;
        });
        setServerData(updatedData);
      });
  }, []);

그리고 구현한 페이지를 확인하니 forEach로 만들었을때와 동일하게 작동하였다.

 

위의 두 코드를 만들고 비교하면서, 리액트에서의 forEach와 map의 사용용도를 나름대로 생각하고 결론을 내렸다.

내가 내린 결론은 위의 코드는 굳이 forEach를 통해서만 만들 필요가 없고 오히려 map을 통해서 만든 코드가 가독성도 좋고 깔끔하다는 것이다.

 

forEach단순하게 요소를 반복하고자 할때만 사용한다. 예를 들면 console.log와 같은 수행을 할때이다.

console.log는 굳이 리액트에서 상태를 변화시킬 필요가 없고 단순하게 콘솔을 찍어내는 기능만한다. 여러개의 데이터를 순환하면서 console을 찍어내야 하는 경우라면, 굳이 map을 통해 새로운 배열을 반환할 필요까지는 없다. 그럴 경우에 forEach를 사용하여 새로운 배열을 반환하지 않으면서 console을 찍어내는 기능을 수행할 것이다.

하지만 이런 특성때문에 불변성을 유지해야하는 리액트에서 forEach만을 사용하여 상태를 변화시키는 것은 어려워보인다. forEach를 통해서 새로운 상태값을 업데이트하려면 다른 변수 (위에서는 list)가 필요하다. forEach를 통해서 특정한 값을 추출하거나, 특정한 행동을 반복하면서 그 변수를 채우도록 하는 것이다. 그렇게 되면 새로운 값을 반환하는 map을 사용하는 것과 큰차이가 없어 보인다. (list를 forEach를 통해서 채우는것과, map을 통해 새로운 배열을 반환하는것의 비교)

그렇다면 굳이 위의 코드에서 forEach를 사용할 필요가 있을까..?

 

이와 반대로 map은 새로운 배열을 반환한다. 그러면 list라는 변수를 따로 선언하고 그것을 push을 통해 채울 필요는 없다. 대신, return을 통해서 값을 반환한다. 새롭게 반환한 값을 이용하기 위해서 const를 통해 updateData라는 변수에 담는다. 

그런다음 setServerData(updataData)를 통해 호출하여 변경된 값을 업데이트한다. 이렇게 하면 따로 list를 만들 필요가 없고 코드의 가독성도 올라간다.

 

 

🔫 내가 내린 결론: 단순한 작업을 할 때를 제외하고는 리액트에서는 웬만하면 forEach보다는 map을 사용해서 순환작업을 한다 

불변성을 유지해야하는 리액트의 특성상 어차피 새로운 배열을 만들어야한다. 그러므로 새로운 배열을 바로 반환하는 map메서드를 사용하여야하는 경우가 훨씬 많을 것이다. 그리고 list와 같이 새로운 배열을 만드는 것보다 가독성도 더 좋다.

 

 

새로운 배열을 반환할 필요가 있는 경우도 따로 정리하였다.

이 특징을 보고 나면 forEach와 어떤 차이가 있는지 조금 이해가 되는것같다.

 

1. 데이터변환: 기존 배열의 각 요소를 가공 ➡️ 새로운 데이터를 생성해야 할때

ex> 배열의 각 숫자를 제곱하여 새로운 배열을 생성, 문자열의 각 요소를 대문자로 변환하여 새로운 배열을 생성

 

2. 필터링: 배열에서 특정 조건을 만족하는 요소만 선택하여 새로운 배열을 생성해야 할때 (filter())

ex> 숫자배열에서 짝수만 선택하여 새로운 배열 생성, 문자열배열에서 특정 문자열을 포함하는 요소만 선택하여 새로운 배열 생성

 

3. 정렬: 배열의 요소를 정렬하여 새로운 배열을 생성해야 할 때 (sort())

ex> 숫자배열을 오름차순, 내림차순으로 정렬하여 새로운 배열을 생성

 

4. 매핑: 배열의 각 요소를 다른 형태로 변환하여 새로운 배열을 생성해야할때 (flatMap())

ex> 다차원배열을 평면화하여 새로운 배열을 생성, 객체배열에서 특정 속성만 선택하여 새로운 배열을 생성

 

 

공식문서에서 forEach를 사용하는것은 예시로 든것이고 예시속의 코드도 console.log와 같이 단순한 작업을 수행하기 때문일 것이라고 이해했다. 따라서 위의 코드는 map을 사용하는 코드로 수정하였다.

(아직 공부중이라서 정확한 내용은 아닐 수 있다. 나름대로 고민하고 내린 결론이 이것인데 만약 아니라면 추후에 수정을 하겠다)

 

 

 

3. 원본 배열의 변경:

  • forEach: forEach는 원본 배열을 직접 수정할 수 있다. 작업 중에 원본 배열을 변경하는 것이 가능
  • map: map은 새로운 배열을 반환하므로, 원본 배열을 직접 수정하지 않는다.

       각 요소를 변환하여 새로운 배열을 생성하는 방식으로 작업을 수행

 

 

➡️ 원본배열을 직접적으로 변경하고 싶을경우에는 forEach를 사용한다.

 

 

 

 

🙋‍♀️ 내 생각

문제를 푸는데는 얼마 걸리지 않았지만 forEach와 map의 차이점을 공부하면서 시간이 많이 걸렸다. 정확히 어떤 차이가 있는지 사용법이 어떻게 달라지는지, 리액트에서는 두개의 메서드를 어떤식으로 활용하면 좋을지를 고민했다. 그래도 이번 기회로 나름 생각도 많이하고 어떤게 다른지 이해도가 조금 깊어진 것 같다. 리액트에서도 앞으로 두개의 메서드를 어떤식으로 활용해야할지 방향이 조금 잡혔다.

두 메서드를 리액트에서 어떤식으로 활용해야할지불변성을 지켜야하는 특성을 지니고 있는 리액트의 특성과 관련하여 생각해보면 좋을 것 같다.

 

 

📚 스스로 공부하고 이해한 내용을 바탕으로 기록한 것이기때문에 부정확한 내용이 포함되어있을 수 있습니다

 

 

참고

 

React Js forEach vs map in JSX

React Js forEach vs map in JSX Last updated : October 16, 2022

www.learnbestcoding.com