---
layout: post
title: '자잘한 도움말 #4 2차원 배열 초기화'
description: 참조의 복사, Python 2차원 배열 초기화, Javascript 2차원 배열 초기화
date: 2022-02-13 18:00:00 +0900
categories: [Micro]
---

## TL;DR
 - N*M 2차원 배열을 한 줄로 초기화 하려면,
   - Python에선 이렇게 한다.
  ```python
  [[0 for i in range(M)] for j in range(N)]
  ```
   - Javascript에선 이렇게 한다.
  ```javascript
  [...Array(N)].map((e) => Array(M).fill(0))
  ```
 - 2차원 배열 생성 시 **참조의 복사**를 항상 조심하자.

## 이하 그리 중요하진 않은 내용들

에 뭐 한 달에 한 번 정도 죽지도 않고 찾아왔습니다. 쓰잘데기 없는 자잘한 도움말 그 네번째. 오늘의 주제는 **2차원 배열 초기화**.(사실 작년에 정해둔 주제인데 이제야 쓴다.)

![나는 바보다](https://i.postimg.cc/SNVDB4gv/image.png)
{: .center}

꾸준히 말했듯 기록하지 않으면 까먹는다. 여기서 출발하는 조금 민망한 이야기를 말하자면, 나는 자꾸 2차원 배열 초기화 방법을 까먹는다. 까먹는다기보단, 하라면 하는데, 자꾸 '아 이거 한 줄로 하는 방법 있었는데' 하고 뇌정지가 온다.

작년 진행했던 프로젝트에서 나는 파이썬을 사용해서 데이터를 분석하는 에이전트 프로그램을 만들고 있었다. 당시의 본인에 잠시 이입해서 글을 작성하겠다. 나는 2차원 배열을 만들어야 했다. 당연히 기억이 안났고, 굳이 그걸 또 스택 오버플로우에 안찾아보고 내가 해봐야지 하고 다음과 같은 코드를 사용해봤다.

```python
data = [[0] * NofColumn] * NofRow
```

그리고 그 결과,  

![멍첨함의 말로](https://i.postimg.cc/cHhM3MtL/image.png)  
본인은 이걸 **무지의 말로**라고 부르기로 했습니다.
{: .center}

일단 원인은 모르겠는데 뭔가 이상하게 작동한다. 바로 항복하고 스택 오버플로우의 도움을 청했다.

```python
[[0 for i in range(NofColumn)] for j in range(NofRow)]
```

![지혜](https://i.postimg.cc/GhW7dKkW/image.png)  
이건 **옛 현인들의 지혜**라고 부르기로 했습니다.
{: .center}

당시엔 이렇게 검색해서 문제를 해결했고. 따로 메모만 해뒀다: 나중에 그 이유를 찾아서 공부하자. 아무래도 그때는 좀 데드라인에 치여서 바빴던지라... 아무튼 오늘은 이 현상에 대해서 공부해보려고 한다.

### 먼저, 현상에 대해
![js](https://i.postimg.cc/MTB0P2LX/js.png)  
일단 말해두자면, 파이썬만의 문제는 아니다.
{: .center}

무슨 일이 일어나고 있는거지? 나는 배열의 [1][0] 요소를 바꿨는데, [0][0] 요소의 값도 바뀌어버렸다.

![차이](https://i.postimg.cc/T2gMmKtv/image.png)  
{: .center}

계속해서 이어지는 테스트. 마지막 두 줄을 보자. `===` 연산자에 대해선 다들 아시리라. `data0`과 `data1`은 개별적으로 선언되어 각자 다른 메모리 주소를 가지고 있으며, `===` 연산자로 비교하면 둘의 메모리 주소가 다르기 때문에 `false`를 리턴한다.  

하지만 눈에 보이기엔 서로 떨어진 다른 배열처럼 보이는 `data[0]`과 `data[1]`을 비교하면 `true`가 반환된다. **둘의 메모리 주소는 같다.**

![WhatI](https://i.postimg.cc/N007qCN7/whati.png)  
이렇게 된다는 뜻
{: .center}

### 왜 이렇게 됐을까?

내가 저지른 코드를 다시 보자.  

```javascript
const data = new Array(2).fill(new Array(3).fill(0))
```

1. 길이가 2인 새 배열을 생성한다. (`new Array(2)`)
2. 생성한 배열을 채워넣는다. (`.fill`)
3. 채워넣을 값은 길이가 3인 새로운 배열이다. (`(new Array(3)`)
4. 새 배열을 0으로 채워넣는다. (`.fill(0))`)

문제는 3번에서 발생한다. `fill()` 메서드에 대한 설명을 읽어보자.

[https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/fill](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/fill)

> fill() 메서드는 배열의 시작 인덱스부터 끝 인덱스의 이전까지 정적인 값 하나로 채웁니다.

**정적인 값 하나**. `fill()`은 그저 값 하나를 받는다. 우리가 건내준 것은 무엇인가. `new Array(3)`이라는 코드로 생성한 배열의 주소가 하나 전달된다. `fill()` 메소드 내부적으로 `new Array()`를 반복하는 것이 아니라는 점.

![node](https://i.postimg.cc/nh0XvFHp/node.png)  
built-in 메서드의 구현체에서는 그냥 전달받은 값으로 배열의 값을 변경할 뿐.
{: .center}  

파이썬에서 사용했던 코드 또한 같은 맥락이다.  

```python
x = [1] * 4
l = [x] * 3
print(f"id(x): {id(x)}")
# id(x): 140560897920048
print(
    f"id(l[0]): {id(l[0])}\n"
    f"id(l[1]): {id(l[1])}\n"
    f"id(l[2]): {id(l[2])}"
)
# id(l[0]): 140560897920048
# id(l[1]): 140560897920048
# id(l[2]): 140560897920048

x[0] = 42
print(f"x: {x}")
# x: [42, 1, 1, 1]
print(f"l: {l}")
# l: [[42, 1, 1, 1], [42, 1, 1, 1], [42, 1, 1, 1]]
```  
[https://stackoverflow.com/questions/240178/list-of-lists-changes-reflected-across-sublists-unexpectedly](https://stackoverflow.com/questions/240178/list-of-lists-changes-reflected-across-sublists-unexpectedly)

`[x]*3`을 하면 근본적으로 우리는 배열 `[x, x, x]`를 얻는다. 이는 같은 `x`를 지칭하는 3개의 참조를 가진 배열이다. 따라서 우리는 `for` 문을 사용해서 각 위치에 새로운 배열을 생성해줘야한다.

### 어디선가 느껴지는 기시감
값이 복사될 줄 알았는데... 사실 복사된 것은 주소였어요... [지난달에 작성했던 글](/micro/2022/01/19/micro-tip-3.html)이 생각난다. 배열 뿐만이 아니라 다른 Reference 타입의 데이터에도 이와 비슷한 일들이 일어날 것이다. **Object references are passed by value**. 값과 참조. 그 사이의 미묘한 장난들에 대해 항상 조심하자.

## 내가 배운 것

- 자바스크립트 `fill()` 메소드는 정적 값 하나를 받아 배열을 채운다.
- 파이썬 배열에 대한 `*` 연산자도 정적 값(이 또한 역시 주소 값이 될 수 있다)을 복사한다.
- 즉 전달하는 데이터에 따라 주소가 복사되어 원하지 않는 사이드이펙트가 발생할 수 있다.
- 사실 list와 array는 구분되는 개념이다. 다만 편의상 파이썬에서 list를 배열로 취급한다.
  - 파이썬에도 Array가 따로 있긴 하다. (import array)
- [nodejs 프로젝트는 오픈소스](https://github.com/nodejs/node)라서 우리도 그냥 코드 뜯어볼 수 있다.