[1004] JS문법복습 - 원시 vs 객체 타입, 배열 메서드, Date 객체

방법이있지·2025년 10월 4일

웹개발

목록 보기
11/19

일단 오늘도 이력서 다듬기를 했는데, 뭔가 지금까지 한 프로젝트들이 잘 정리가 안 되니까 이력서도 어떻게 정리해야 할지 헷갈리는 감이 있다.

그래서 내일은 이력서 말고 포트폴리오를 작업하면서 지금까지 한 프로젝트들을 좀 다시 정리해볼 생각이다.

참고: 인프런 <한입크기로 잡아먹는 리액트>

원시 vs 객체 타입

JavaScript에서 변수에 할당되는 값들은 메모리에 저장이 된다.

이때 실제로 값이 저장되는 방식은, 값이 원시 타입인지, 객체 타입인지에 따라 달라진다.

원시 타입 (불변 타입)

  • number, string, boolean, null, undefined 등 타입은 원시 타입이다.
  • 값 자체가 메모리에 저장되며, 변수는 실제 값을 가리킨다.

  변수 선언:  let p1 = 1;
  
     변수          메모리
   ┌──────┐     ┌──────┐
   │  p1  │---->1   │  메모리 주소: 0x001
   └──────┘     └──────┘

  복사:  let p2 = p1;
  
     변수          메모리
   ┌──────┐     ┌──────┐
   │  p1  │---->1   │  메모리 주소: 0x001
   └──────┘     └──────┘
   ┌──────┐     ┌──────┐
   │  p2  │---->1   │  메모리 주소: 0x002 (새로운 복사본!)
   └──────┘     └──────┘
  • p2p1 값을 할당하면, 메모리상 값이 복사되어 독립적인 메모리에 저장된다.
  • 따라서 p2p1은 서로 영향을 주지 않는다.
  재할당:  p2 = 2;
  
     변수          메모리
   ┌──────┐     ┌──────┐
   │  p1  │---->1   │  메모리 주소: 0x001 (변경 없음)
   └──────┘     └──────┘
                ┌──────┐
   ┌──────┐     │  1   │  메모리 주소: 0x002 (버려짐)
   │  p2  │-╳   └──────┘
   └──────┘ ║   ┌──────┐
            ╚═=>2   │  메모리 주소: 0x003 (새 값!)
                └──────┘
  • p2 = 2와 같이 변수에 값을 재할당하더라도, 기존의 값 1은 메모리에서 수정되지 않는다.
    • 그래서 원시 타입을 불변 타입으로도 부른다.
  • 단지 메모리에 새로운 값 2가 새로 저장되고, 변수 p2가 새로운 값을 가리킬 뿐이다.

객체 타입

  • object, array, function 등 타입은 객체 타입이다.
  • 실제 객체 및 해당 객체의 참조값(주소)이 모두 메모리에 저장되며, 변수는 참조값을 가리킨다.
    • 배열의 원소나 객체의 프로퍼티가 추가/삭제/수정될 수 있기 때문에 이러한 방식으로 저장된다고 한다.
변수 선언:  let o1 = { place: "서울" };
  
     변수          참조값           실제 객체
   ┌──────┐     ┌────────┐      ┌─────────────────┐
   │  o1 │---->0xA001----->│ place: "서울"   │  메모리: 0xA001
   └──────┘     └────────┘      └─────────────────┘
  • 변수 o1에는 실제 객체를 가리키는 참조값이 저장되며, 실제 데이터는 별도 메모리에 존재한다.
  얕은 복사:  let o2 = o1;
  
     변수          참조값           실제 객체
   ┌──────┐     ┌────────┐      ┌─────────────────┐
   │  o1 │---->0xA001--┐   │ place: "서울"   │  메모리: 0xA001
   └──────┘     └────────┘  │   └─────────────────┘
   ┌──────┐     ┌────────┐  │          ▲
   │  o2 │---->0xA001--┘         │
   └──────┘     └────────┘             │
                                동일한 객체를 가리킴!
  • o2o1 값을 할당할 때, 메모리상 실제 객체가 아닌 참조값이 복사된다. (얕은 복사)
  • 따라서 o2o1동일한 객체를 가리키므로 서로 영향을 줄 수 있다.
  값 수정:  o2.place = "인천";
  
     변수          참조값           실제 객체
   ┌──────┐     ┌────────┐      ┌─────────────────┐
   │  o1  │---->0xA001--┐   │ place: "인천"   │  메모리: 0xA001
   └──────┘     └────────┘  │   └─────────────────┘
   ┌──────┐     ┌────────┐  │          ▲
   │  o2  │---->0xA001--┘         │
   └──────┘     └────────┘             │
                                 둘 다 영향받음!
                                   
  console.log(o1.place)	// "인천"
  • o2place인천으로 바꾸면, 실제객체(0xA001)의 데이터가 바뀌므로o1.place인천으로 변경된다.
    • 그래서 원시 타입을 가변 타입으로도 부른다.

깊은 복사

  • 만약 원래 객체를 복사해야 하는데 위처럼 의도치 않게 값이 삭제되는 일을 막으려면, ...(스프레드 연산자)로 깊은 복사를 수행해야 한다.
  선언:  let o3 = { place: "서울" };
         let o4 = { ...o3 };
  
     변수          참조값           실제 객체
   ┌──────┐     ┌────────┐      ┌─────────────────┐
   │  o3  │---->0xB001----->│ place: "서울"   │  메모리: 0xB001
   └──────┘     └────────┘      └─────────────────┘
   
   ┌──────┐     ┌────────┐      ┌─────────────────┐
   │  o4  │---->0xB002----->│ place: "서울"   │  메모리: 0xB002
   └──────┘     └────────┘      └─────────────────┘
                                     (새 객체!)
  • ...(스프레드연산자)는 배열/객체의 여러 값을 개별로 흩뿌려 주는 역할을 수행한다.
  • 위와 같이 let o4 = {...o3}로 복사 시, 메모리에 새로운 객체가 생성되며, 프로퍼티만 따로 복사된다.
    • 이를 깊은 복사라 한다.
  값 수정:  o4.place = "대전";
  
     변수          참조값           실제 객체
   ┌──────┐     ┌────────┐      ┌─────────────────┐
   │  o3  │---->0xB001----->│ place: "서울"   │  변경 없음!
   └──────┘     └────────┘      └─────────────────┘
   
   ┌──────┐     ┌────────┐      ┌─────────────────┐
   │  o4  │---->0xB002----->│ place: "대전"   │  o4만 변경!
   └──────┘     └────────┘      └─────────────────┘
  • o3, o4는 별개 객체를 가리키므로, o4.place대전으로 변경해도 o3.place는 변경되지 않는다.

깊은 비교

  • 객체를 비교할 땐, 실제 객체의 값이 아닌 참조값 기준으로 비교가 이루어진다.

  설정:  let o5 = { place: "대구" };
         let o6 = o5;        // 얕은 복사
         let o7 = { ...o5 }; // 깊은 복사


  [얕은 비교 - 참조값 비교]

    o5 === o6  →  true
    
    ┌────────┐      ┌─────────────────┐
    │ 0xC001----->│ place: "대구"   │
    └────────┘      └─────────────────┘
         ▲                   ▲
         │                   │
      o5, o6 모두 같은 참조값!


    o5 === o7  →  false
    
    ┌────────┐      ┌─────────────────┐
    │ 0xC001----->│ place: "대구"   │  o5
    └────────┘      └─────────────────┘
    
    ┌────────┐      ┌─────────────────┐
    │ 0xC002----->│ place: "대구"   │  o7
    └────────┘      └─────────────────┘
    
    참조값이 다름!
  • o6o5와 같은 객체를 가리켜 (동일 참조값) 비교 결과가 true가 된다.
  • o7은 스프레드연산자를 통해 생성된 새로운 객체기 때문에, o5와 프로퍼티의 키/값이 완전히 동일하더라도 참조값이 다르므로 비교 결과가 false가 된다.
  • 이러한 ===를 이용한 참조값 기준 비교를 얕은 비교라고 한다.
  [깊은 비교 - 내부 값 비교]

    JSON.stringify(o5) === JSON.stringify(o7)true
    
    "{"place":"대구"}" === "{"place":"대구"}"
    
    문자열로 변환하여 내부 값 비교!
  • 만약 실제 객체의 값 기준으로 비교를 하고 싶다면, JSON.stringify를 사용해 객체의 값을 문자로 변경한 뒤 내부 값을 비교해야 한다. 이를 깊은 비교라고 한다.

정리

타입저장 방식복사 시수정 시
원시 타입 (불변)값 자체 저장새 값 복사됨 (독립적)새 메모리에 저장 (원본 불변)
객체 타입 (가변)참조값 저장참조값 복사됨 (공유됨)원본 객체 수정 (모두 영향)

배열 메서드

  • 너무 많지만 리액트에서 흔히 사용되는 메서드들 위주로만 정리해보았다.

필수 3형제

map

  • 모든 요소에 대해 콜백 함수 수행 후, 그 반환값으로 이루어진 새로운 배열을 반환한다.
let arr = [1, 2, 3];
let arr_squared = arr.map((item, idx, arr) => {
  console.log(`${idx}번째 요소: ${item}`);
  return item ** 2;
});
// 0번째 요소: 1
// 1번째 요소: 2
// 2번째 요소: 3
console.log(arr_squared); // [1, 4, 9]

let arr = [
  { name: "롯데리아", category: "햄버거" },
  { name: "김가네", category: "김밥" },
  { name: "설빙", category: "디저트" },
  { name: "맥도날드", category: "햄버거" },
];
let food_names = arr.map((item) => item.name);
console.log(food_names); // [ '롯데리아', '김가네', '설빙', '맥도날드' ]
  • 리액트에서는 배열을 JSX 요소 배열로 변환하여, 화면에 리스트를 렌더링할 때 주로 사용된다.
    • 상품 목록, 드롭다운 옵션, 테이블 행 등...
{users.map(user => <UserCard key={user.id} user={user} />)}

filter

  • 콜백 함수 결과가 true인 요소들만 이루어진 새로운 배열을 반환한다.
// 콜백함수 결과가 true인 요소들로만 이루어진 새로운 배열 반환
let arr = [
  { name: "롯데리아", category: "햄버거" },
  { name: "김가네", category: "김밥" },
  { name: "설빙", category: "디저트" },
  { name: "맥도날드", category: "햄버거" },
];
console.log(arr.filter((item) => item.category === "햄버거"));
// [ { name: '롯데리아', category: '햄버거' }, { name: '맥도날드', category: '햄버거' } ]
  • 리액트에서는 검색, 카테고리에 따라 조건에 맞는 데이터만 추출해서 보여줄 때 주로 사용된다.
const activeUsers = users.filter(user => user.isActive);

find

// 배열에서 콜백함수 결과가 true인 첫 요소를 반환. 없으면 undefined 반환
let arr = [1, 2, 3];
console.log(arr.find((item) => item > 1)); // 2

let objectArr = [{ name: "누렁이" }, { name: "탱고" }, { name: "빵아지" }];
console.log(objectArr.find((item) => item.name === "탱고"));
// { name: '탱고' }
  • 리액트에선 파라미터, ID를 기반으로 데이터를 찾아 상세 페이지를 렌더링할 때 주로 사용된다.
const user = users.find(u => u.id === userId);

덜 중요한데 쓰이긴 함

slice

  • 배열의 특정 범위를 잘라, 새로운 배열로 반환한다.
let arr = [1, 2, 3, 4, 5];
console.log(arr.slice(1, 4)); // [2, 3, 4] (인덱스 1부터 4 전까지)
console.log(arr.slice(2)); // [3, 4, 5] (인덱스 2부터 끝까지)
console.log(arr.slice(-2)); // [4, 5] (뒤에서부터 2개)
console.log(arr); // [1, 2, 3, 4, 5] (원본 배열은 변경되지 않음)
  • 리액트에선 페이지네이션으로 현재 페이지에 해당하는 데이터만 잘라내거나, 원본 배열을 유지하며 특정 요소를 삭제할 때 사용한다.
// 페이지네이션 예시
const currentPage = 2;
const itemsPerPage = 10;
const displayItems = items.slice(
  (currentPage - 1) * itemsPerPage,
  currentPage * itemsPerPage
);
const newArr = [...arr.slice(0, index), ...arr.slice(index + 1)];

sort/toSorted

  • 사전 순으로 배열 요소를 정렬한다. 원본 배열이 변경된다!!
  • 사전 순이므로, 숫자값 등을 비교할 시 별도 비교 콜백 함수로 명시해야 한다.
    • 매개변수 2개를 받으며, -1 반환 시 앞 변수, 1 반환 시 뒷 변수가 앞에 오게끔 정렬된다.
// 사전순으로 배열 요소를 정렬. 원본 배열이 변경됨
let arr = [5, 10, 3];
arr.sort();
console.log(arr); // [10, 3, 5] (사전순임에 유의)

// 숫자값 기준 시, 비교 콜백함수로 명시해야 함
arr.sort((a, b) => {
  if (a > b) {
    // 양수 반환 -> 뒤의 매개변수(b)가 먼저 옴
    return 1;
  } else if (a < b) {
    // 음수 반환 -> 앞의 매개변수(a)가 먼저 옴
    return -1;
  } else {
    // 0 반환 -> 두 값의 자리를 바꾸지 않음
    return 0;
  }
});
console.log(arr); // [3, 5, 10]

let arr_desc = [5, 10, 3];

// 내림차순일 시 그 반대로...
arr_desc.sort((a, b) => {
  if (a > b) {
    return -1;
  } else if (a < b) {
    return 1;
  } else {
    return 0;
  }
});
  • 리액트에서는 최신순, 가격순 등 정렬 기능을 구현할 때 사용된다.
    • 반드시 복사본을 만든 후 정렬해야 한다.
const sorted = [...items].sort((a, b) => a.order - b.order);
  • 원본이 유지되고 새로운 배열을 반환하는 toSorted를 사용할 수도 있다.
const sorted = items.toSorted((a, b) => a.price - b.price);

알면 좋은 나머지 메서드 요약

내가 다 정리하기 귀찮아서 클로드가 요약해줬다.

메서드설명자주 쓰나?React에서 언제?대안
findIndex조건 만족하는 첫 요소의 인덱스 반환⭐⭐⭐특정 요소 수정/삭제 시-
includes특정 요소 포함 여부 확인⭐⭐⭐선택 여부, 권한 체크-
forEach모든 요소 순회하며 작업 수행 (반환값 필요 없을 때)⭐⭐useEffect 내 부수효과 처리map 선호
join배열을 문자열로 결합⭐⭐태그, 주소 등 문자열 표시-
concat두 배열 합치기무한 스크롤 데이터 추가[...arr1, ...arr2] 선호
indexOf요소의 인덱스 반환원시값 인덱스 찾기includes, findIndex 더 직관적
push마지막에 요소 추가 (원본 변경)사용 금지[...items, newItem]
pop마지막 요소 제거 (원본 변경)사용 금지items.slice(0, -1)
shift첫 요소 제거 (원본 변경)사용 금지items.slice(1)
unshift첫 요소 추가 (원본 변경)사용 금지[newItem, ...items]

리액트에서 push, pop, shift, unshift 쓰면 안되는 이유

  • 리액트에서는 상태 변화를 참조값(메모리주소) 비교로 감지한다.
  • 아예 새로운 배열을 만든 경우 참조값이 변경돼서 변화를 감지할 수 있다.
  • 하지만 원본 배열을 직접 수정하면, 참조값은 그대로이므로 변화를 감지할 수 없다.
// 올바르지 않은 방법
const [items, setItems] = useState([1, 2, 3]);

const handleAdd = () => {
  items.push(4);        // 원본 배열 수정
  setItems(items);      // 같은 참조(주소) -> React는 변화 없다고 판단
};
// 화면이 업데이트되지 않음

// 새로운 배열 생성 - React가 변화 감지
const handleAdd = () => {
  setItems([...items, 4]);  // 새 배열 생성 -> 다른 참조(주소)
};
// 화면이 정상적으로 업데이트됨

Date 객체와 날짜

Date 객체

  • 후술할 각종 메서드를 사용할 수 있다.
let date1 = new Date(); // 매개변수 없을 시현재 시간
console.log(date1);
// Sat Oct 04 2025 22:17:15 GMT+0900 (한국 표준시)
// 결과는 실행 시점에 따라 다름

let date2 = new Date(2000, 6, 22, 12, 30, 59);
// 월은 0부터 시작함에 유의 (0: 1월, 1: 2월, ..., 6: 7월)
console.log(date2); // Sat Jul 22 2000 12:30:59 GMT+0900 (한국 표준시)

타임 스탬프

  • 특정 시간이 1970년 1월 1일 00:00:00 UTC로부터 몇 밀리초가 지났는지 나타낸 값
  • 참고로 대한민국은 UTC보다 9시간 빠르다.
let ts1 = date2.getTime();
console.log(ts1); // 964296659000

// 타임 스탬프로부터 Date 객체 생성 가능
let date3 = new Date(ts1);
console.log(date3); // Sat Jul 22 2000 12:30:59 GMT+0900 (한국 표준시)

시간 요소의 추출, 수정

  • 추출은 get, 수정은 set을 기억하자.
// 시간 요소 추출
let year = date2.getFullYear();
let month = date2.getMonth() + 1; // 월은 0부터 시작하므로 +1
let day = date2.getDate();
let hours = date2.getHours();
let minutes = date2.getMinutes();
let seconds = date2.getSeconds();
console.log(
  `${year}${month}${day}${hours}${minutes}${seconds}`
);
// 2000년 7월 22일 12시 30분 59초

// 시간 요소 수정
date2.setFullYear(2025);
date2.setMonth(9); // 10월 (0부터 시작)
date2.setDate(4);
date2.setHours(22);
date2.setMinutes(15);
date2.setSeconds(30);
console.log(date2); // Sat Oct 04 2025 22:15:30 GMT+0900 (한국 표준시)

시간 포맷팅

console.log(date2.toDateString()); // Sat Oct 04 2025
console.log(date2.toLocaleString()); // 2025. 10. 4. 오후 10:15:30
console.log(date2.toLocaleDateString()); // 2025. 10. 4.
profile
뭔가 만드는 걸 좋아하는 개발자 지망생입니다. 프로야구단 LG 트윈스를 응원하고 있습니다.

0개의 댓글