프론트엔드 개발을 하면서 데이터를 리스트 형태로 다뤄야 할 일이 많았다. 그런데 대학생때 익숙하게 손이 가던 arr[0] =3; 식의 C배열 방식은 JS에선 뭔가 잘 안 맞는 느낌이 들었다. 처음에는 단순히 문법 차이라고 생각했지만, 쓰면 쓸수록 배열의 “본질”이 다르다는 느낌이 강하게 들었다.
그래서 이번 기회에 C언어와 JavaScript배열의 차이를 파고들어 봤고, 이 글은 그 차이를 학습하고 기록한 탐구?일지이다.
배열은 정말 배열일까?
우리가 흔히 아는 ‘배열’이란 무엇인가.
대학교에서 배열을 배우면 보통 C언어에서 시작한다. 그럴 때 배열은 이런 느낌이다.
int arr[3] = {1, 2, 3};
- ‘크기가 고정’된 숫자 3개짜리 박스 → arr[3] 이면 3개 고정
- 0번 인덱스부터 시작해서 인덱스를 통해 접근하는 방식 → 인덱스로 접근해서 빠르게 O(1) 접근 가능
- 모든 값은 같은 자료형(Int)
- 메모리 상에 연속된 공간에 저장 → arr[0], arr[1], arr[2]는 연속된 주소에 저장됨
이런 배열을 정의 내리자면, “같은 타입의 값들이 연속된 메모리 공간에 저장된 것“
즉, 배열:Array 개념은 “같은 종류의 데이터들을 연속적으로 저장하는 구조”라는 이미지로 머릿속에 각인된다.
C언어 배열의 내부 구조를 보면,
#include <stdio.h>
int main() {
int arr[3] = {10, 20, 30};
printf("arr[0] = %d, 주소: %p\n", arr[0], &arr[0]);
printf("arr[1] = %d, 주소: %p\n", arr[1], &arr[1]);
printf("arr[2] = %d, 주소: %p\n", arr[2], &arr[2]);
return 0;
}
대략 이런 식으로 출력된다.
arr[0] = 10, 주소: 0x7ffee11c04d0
arr[1] = 20, 주소: 0x7ffee11c04d4
arr[2] = 30, 주소: 0x7ffee11c04d8
- int는 보통 4바이트니까, 주소가 4씩 증가하고 있는 거고
- C배열은 ‘같은 타입 데이터’가 연속된 메모리 블록에 저장된다는 걸 확인할 수 있다.
그런데 자바스크립트에서는 뭔가 다르네?
JavaScript에서 배열을 처음 선언할 때 이렇게 한다.
const arr = [01, 'jinuk', true];
??? C언어 배열이랑 뭔가 다르지 않나?
- 숫자, 문자열, 불리언이 섞여 있음
- 그리고 arr.push('new') 같은 메서드로 길이도 바꿀 수 있음.
- typeof arr를 찍어보면 “object”로 나옴
즉, JavaScript의 배열은 우리가 알고 있던 배열이 아니다.
차이점이 뭘까?
항목 | C언어 배열 | JavaScript 배열 |
자료형 | 단일 타입 (int, char) | 혼합 가능 (1, 'a', true) |
크기 | 고정된 크기 | 동적 크기 |
메모리 | 연속된 공간 | 내부적으로 객체 구조 |
메서드 | 없음 (직접 반복문이 쓰여야 함) | push, pop, map, filter, reduce 등 매우 다양함 |
정체? | 진짜 배열 | 특수한 형태의 객체 (Array is Object!!!!) |
C언어 예제
int arr[3] = {1, 2, 3};
arr[3] = 4; // 범위 초과로 오류남
JavaScript 예제
const arr = [1, 'jinuk', true];
arr[3] = 4; // 4가 추가됨
console.log(arr); // [1, 2, 3, 4]
예제에서 보이는 것처럼 JS에서는 배열도 그냥 객체니까, arr[3] = 4처럼 새로운 속성을 추가하는 것이 허용되는 것이다. 즉, 진짜 배열이 아니라 ‘객체처럼 동작하는 유사 배열’이라고 할 수 있을 듯하다.
JavaScript 배열은 객체라고?
const arr = [10, 20, 30];
console.log(typeof arr); // "object" 라고 나옴
console.log(Object.keys(arr)); //["0", "1", "2"]
arr[0]은 사실 arr[“0”]과 같은 표현이다. 인덱스를 키로 가지는 특수한 객체라는 뜻인데, 이 때문에 배열은 자바스크립트에서 매우 유연하지만, 조금 헷갈리는 느낌이 있는 듯하다.
객체란, “이름표가 붙은 상자들의 모음”이다.
- 배열은 [0], [1], [2] 라는 숫자 인덱스가 자동으로 붙은 상자들이고,
- 객체는 { name, age, speak }처럼 내가 직접 이름표(key)를 붙여서 만든 상자들의 모음이다.
const arr = [1, 'Alice', true]
이런 arr 유사 배열(객체)의 내부 구조는 이렇게 되어 있다.
JS엔진이 해석하는 유사 배열의 내부 구조:
{
"0": 1, // key: "0", value: 1
"1": "Alice", // key: "1", value: "Alice"
"2": true, // key: "2", value: true
length: 3, // 배열 길이: 3
__proto__: Array.prototype // 배열 메서드를 갖고 있는 원형 객체 (프로토타입)
}
// __proto__는
이 배열이 상속하는 원형 객체 (Array.prototype).
여기에 push(), pop(), map() 같은 배열 메서드가 있다는 의미를 가짐.
// 확인해보고 싶다면?
console.log(Object.keys(arr)); // ["0", "1", "2"]
console.log(typeof arr); // "object"
console.log(arr instanceof Array); // true
console.log(arr.__proto__ === Array.prototype); // true
즉, 배열도 결국은 숫자 키를 가진 객체일 뿐이다.
TypeScript에서는 어떨까?
TypeScript는 JavaScript의 확장이다. JavaScript의 유연함은 유지하되, 타입을 명시적으로 지정할 수 있도록 해주는 MS에서 만든 개발자를 위한 언어이다.
const nums: number[] = [1, 2, 3];
const words: string[] = ['apple', 'banana', 'grape'];
const mixed: (string | number)[] = [23, 'jinuk'];
- JavaScript의 배열은
- 동적이고 유연하지만 타입 체크가 없다 → 실수 유발 가능성 증가
- TypeScript의 배열은
- 동일한 구조지만 타입을 제한 → 안정석 확보!
이런 차이가 있다. JavaScript에서 이런 타입 실수가 일어나면 컴파일 단계에서 잡는 게 아니라 런타임 단계에서 잡아야 하기때문에 상당히 번거로운데, TypeScript는 이런 경우를 방지해줘서 많이 쓰이고 있다.
그럼 실제 배열은 어떻게 쓰일까? (with React)
배열이 React에서 중요한 이유?
React는 UI를 상태 기반으로 선언적으로 구성한다. 어떤 배열이 있다고 하면, 그 배열의 상태에 따라 UI가 자동으로 변화하길 기대하는 것이다.
이게 무슨 말이냐 하면:
JavaScript가 DOM을 조작하는 건 화이트보드에 손으로 글씨를 쓰고 지우는 일이라고 볼 수 있는데,
“2번 아이템 삭제됐네? 그럼 지우고, 나머지 다 위(기존 2번 아이템 공간)로 올려야겠다”
같은 일이다.
반면, React는 상태(state)라는 종이를 주고, 그 종이를 보고 React가 자동으로 화이트보드에 그림을 그려주는 방식이다.
React: “이 배열이 지금 [1,2,3]이야? 그럼 이렇게 그릴게"
배열: “나 [1,3]으로 바꼈는데?”
React: “2번 사라졌네? 내가 알아서 UI 바꿔줄게”
이런 식이다.
이럴 때 배열은 단순히 데이터를 저장하는 것 뿐만 아니라, 렌더링 로직의 핵심이 된다.
(이미지는 React의 Virtual DOM을 이용한 렌더링 흐름)
백엔드에서 데이터 호출 후 React로 리스트 렌더링 하기
보통 프론트엔드 작업할 때 배열이 쓰이는 경우는 백엔드와 연동될 때 쓰인다.
1. 백엔드 응답 예시 (Rest API)
GET /api/posts
[
{
"id": "1",
"title": "JavaScript 배열",
"author": "정진욱"
},
{
"id": "2",
"title": "React 배열 사용 예시",
"author": "정코코"
}
]
- 보통 백엔드에선 JSON 형태의 배열 데이터를 응답한다.
- 이 배열이 React의 map()을 타고 컴포넌트로 분해되는 것이다.
2. 프론트엔드 컴포넌트
type Post = {
id: string;
title: string;
author: string;
};
export const PostCard = ({ title, author }: Post) => {
return(
<div className="border p-4 rounded shadow-sm">
<h2 className="text-xl font-semibold">{title}</h2>
<p className="text-sm text-gray-600">by {author}</p>
</div>
)
}
3. 리스트 컴포넌트
import { useEffect, useState } from 'react';
import { PostCard } from '../components/PostCard';
type Post = {
id: string;
title: string;
author: string;
};
export const PostList = () => {
const [posts, setPosts] = useState<Post[]>([]);
useEffect(() => {
fetch('/api/posts')
.then((res) => res.json())
.then((data: Post[]) => setPosts(data));
}, []);
return (
<div className="">
{posts.map((post) => (
<PostCard key={post.id} {...post} />
))}
</div>
)
}
- useEffect로 페이지 로드 시 API를 요청하고
- 응답 데이터를 Post[] 배열로 받아 setPosts()에 저장
- posts.map()으로 각 요소를 PostCard 컴포넌트로 분해 렌더링
여기서 중요한 건,
포인트 | 설명 |
배열은 데이터 리스트를 표현: | 백엔드 응답 자체가 배열 형태임 |
map()은 반복 렌더링의 핵심: | React에서 UI리스트는 map 기반으로 이루어짐 |
key는 고유한 식별자로 필수: | post.id와 같은 유니크 값이 없으면 경고 발생함. 왜?
|
TypeScript로 타입 안정성 확보: | 응답 구조를 Post 타입으로 고정하여 타입으로 인한 오류 방지 |
끝내는 말
전에는… ‘아~ 배열이라는 게 있구나~’, ‘아~ 여기서 배열이 쓰이는구나~’ 생각하고 넘어갔었는데, 이번 기회에 내가 알던 배열과 프론트 개발하면서 쓰던 배열이 어떻게 다른지를 확실히 알게되었다. React에서 배열은 UI의 구조를 결정하고 렌더링의 핵심 로직이었는데, 그리고 key 없이 코딩해서
Warning: Each child in a list should have a unique "key" prop.
이런 에러도 많이 봤었는데, 왜 이제서야 의문을 품은 걸까,, 그래도 이번 기회에 확실하게 알게 되었으니 다음부터는 반영해서 조금 더 안정적으로 개발을 해보도록 하자.
-끝-
'Frontend Dev Log' 카테고리의 다른 글
TailwindCSS v4!! v3와 달라진 점!!!! (0) | 2025.05.12 |
---|---|
디자인 시스템, 프론트엔드 개발자는 어디까지 알아야 할까? (0) | 2025.05.05 |