꼭 알아야 할JS 개념 (리액트 Deep Dive)

hour_2·2025년 11월 8일

프론트엔드

목록 보기
3/5

개발을 하기 위해 리액트를 공부하고자 한다면 자바스크립트에 대해서 어느정도 알아야 한다.
리액트만 공부하는 건 불가능하다 !!

따라서 오늘은 리액트를 공부하기 이전에 꼭!!! 필요한 JS 개념에 대해서 알아보려고 한다.


💡 자바스크립트의 동등 비교

데이터 타입

원시타입

  • boolean
  • null
  • undefined
  • number
  • string
  • symbol
  • bigint

객체타입

  • object

null & undefined의 차이점 ?

undefined: 선언한 후 값을 할당하지 않은 변수 또는 값이 주어지지 않은 인수에 자동으로 할당되는 값

null: 아직 값이 없거나 비어 있는 값을 표현할 때 사용

생성방식

let a;
console.log(a); // undefined (값이 할당되지 않음)

let b = null;
console.log(b); // null (명시적으로 '비어 있음'을 표현)
  • undefined는 변수가 선언되었지만 값이 전혀 지정되지 않았을 때 자동으로 할당됨.
  • null은 개발자가 명시적으로 비어 있음을 표현하기 위해 직접 할당함.

동등성 비교

undefined == null;  // true  (값이 없다는 의미로 느슨하게 같음)
undefined === null; // false (타입이 다름)
  • 느슨한 비교(==)에서는 “없음”이라는 공통점 때문에 true.
  • 엄격한 비교(===)에서는 타입이 다르기 때문에 false.

JSON 직렬화 시 차이

JSON.stringify({ a: undefined, b: null }); 
// '{"b":null}'
  • undefined 프로퍼티는 제외됨.
  • null은 그대로 포함됨 → “의도적 비어 있음”으로 간주.

타입 비교

typeof undefined; // "undefined"
typeof null;      // "object"
  • undefined는 고유 타입.
  • null은 역사적 이유로 "object"를 반환하지만, 실제로는 원시 타입(primitive).

null의 특별한 점!
=> null의 type을 확인하면 object라는 결과가 나온다 !

typeof null === 'object' 는 자바스크립트 초기 버전의 비트 기반 타입 태그 설계에서 비롯된 버그로 null이 객체 타입 + null 포인터로 표현되었기 때문에 발생한다.
이는 현재로써 해결할 수 있는 기술이지만 이 문제가 수정되면 기존 코드에 부정적인 영향을 미칠 수 있기 때문에 오늘날까지 유지되고 있는 버그이다.

undefined와 null의 차이점을 한 줄로 설명하자면,
undefined선언됐지만 할당되지 않은 값이고, null명시적으로 비어 있음을 나타내는 값으로 사용하는 것이 일반적이다.

값 저장 방식의 차이

원시 타입

let hello = ’hello world'
let hi = 'hello world’

console.log(hello === hi) // true
  • hi와 hello 값을 비교하면 true가 나온다 ? => 정답

객체 타입

// 다음 객체는 완벽하게 동일한 내용을 가지고 있다.
var hello = {
greet: 'hello, world',
}

var hi = {
greet: 'hello, world',
}
// 그러나 동등 비교를 하면 false가 나온다.
console.log(hello === hi) // false
// 원시값인 내부 속성값을 비교하면 동일하다.
console.log(hello.greet === hi.greet) // true
  • 완벽하게 동일한 내용을 가지고 있는 객체 hello와 hi를 비교하면 true가 나온다 ? => 오답 ㅜ

왜 객체는 객체 안에 동일한 값을 가지고 있어도 값을 비교하면 false이 나올까?

  • 원시 타입은 불변 형태의 값을 저장하고 이 값은 변수 할당 시점에 메모리 영역을 차지하고 저장
  • 객체는 값을 저장하는 게 아니라 참조를 저장하기 때문에 앞서 동일하게 선언했던 객체라 하더라도 저장하는
    순간 다른 참조를 바라보기 때문에 false를 반환 !!

값을 복사한다면 ?

var hello = {
greet: ’hello, world',
}

var hi = hello
console.log(hi === hello) // true
  • var hi = hello 를 실행하면 객체의 값이 아니라 “주소”가 복사되기 때문에 true !

Object.js

==, === 와는 또다른 리액트에서 제공하는 비교 방법

사용방법

-0 === +0 // true
Object.is(-0, +0) // false

Number.NaN === NaN // false
Object.is(Number.NaN, NaN) //true

NaN === 0 / 0 // false
Object.is(NaN, 0/0) //true
  • ==, === 만으로는 정확하게 구별하지 못하는 특이한 케이스를 비교하기 위해 만들어짐

하지만 객체 비교에서는 여전히 참조(주소) 비교를 하기에 개발자가 의도한대로 원할한 비교가 되지 않는다 ㅜ

Object.is({}, {}) // false

shallowEqual

Object.js의 객체 비교 문제를 해결하기 위한 얕은 비교 함수

function shallowEqual(objA, objB) {
  // 1. Object.is로 먼저 완전 동일한지 비교
  if (is(objA, objB)) return true;

  // 2. 둘 중 하나라도 객체가 아니면 false
  if (typeof objA !== 'object' || objA === null ||
      typeof objB !== 'object' || objB === null) return false;

  // 3. 키 배열 길이가 다르면 false
  const keysA = Object.keys(objA);
  const keysB = Object.keys(objB);
  if (keysA.length !== keysB.length) return false;

  // 4. 같은 키를 순회하며 Object.is로 각 값 비교
  for (let key of keysA) {
    if (!objB.hasOwnProperty(key) || !is(objA[key], objB[key])) return false;
  }

  return true;
}
  • shallowEqual은 객체의 1 depth(첫 번째 깊이) 까지는 값을 비교

얕은 비교의 한계

리액트에서는 props가 대부분 단순한 값(문자열, 숫자) 이라서 1 depth까지만 비교해도 대부분 충분하지만 깊은 객체에서는 문제가 생긴다.

const Component = memo((props) => {
  console.log("렌더링됨");
  return <h1>{props.counter}</h1>;
});

const DeeperComponent = memo((props) => {
  console.log("렌더링됨");
  return <h1>{props.counter.counter}</h1>;
});

<Component counter={100} />
<DeeperComponent counter={{ counter: 100 }} />
  • 첫 번째 Component는 counter가 원시값이므로 비교 가능 → memo가 잘 작동함

  • 두 번째 DeeperComponent는 counter가 객체라서 매 렌더링마다 { counter: 100 }이 새로 생성됨 → 참조가 달라짐 → shallowEqual에서 false

=> 즉, 내부 값은 같지만 객체의 참조가 바뀌어 memo가 무효

근데도 깊은 비교가 아닌 얕은 비교를 하는 이유?

성능의 문제점
객체 안에 객체가 몇 개 들어있을지 모르기 때문에 재귀로 비교하다 보면 렌더링마다 모든 값 전체를 순회해야 한다.

그래서 리액트는 얕은 비교까지만 지원하고 더 깊은 구조를 비교해야 할 땐 개발자가 직접 useMemo, useCallback 등으로 불필요한 객체 생성이나 참조 변경을 막아야 한다.


💡 함수

함수를 정의하는 4가지 방법

1. 함수 선언문

hello();  // "hi"
function hello(){ console.log("hi"); }
  • 함수 자체가 호이스팅 됨 => 선언이 스코프 맨 위로 끌어올려져 선언 전 호출 가능.

2. 함수 표현식

console.log(typeof hello); // "undefined"
hello();                   // TypeError
var hello = function(){};
  • 변수에 함수를 할당
  • 호이스팅은 변수 hello만 됨 !

3. Function 생성자

const add = new Function('a', 'b', 'return a + b');
  • 매개변수/본문을 문자열로 작성 → 가독성·성능·보안 불리
  • 클로저가 안 생김

4. 화살표 함수

add() //error
const add = (a, b) => a + b;
  • 호이스팅 X
  • this 바인딩 없음 → 상위 스코프의 this를 캡처(렉시컬 this).
  • arguments 없음 → ...rest 사용.
  • new로 생성자 호출 불가, prototype 없음.
  • 메서드로 쓸 땐 일반 함수 권장(특히 클래스/객체 메서드).

클래스나 객체 메서드 내부에서는 화살표 함수 대신 일반 함수를 쓰기

선언문과 표현식의 차이

선언문과 표현식의 가장 크고 중요한 차이는 바로 호이스팅 !!

호이스팅이 뭔데 ?

MDN 공식문서에서 말하는 호이스팅이란

인터프리터가 코드를 실행하기 전에 함수, 변수, 클래스 또는 임포트(import)의 선언문을 해당 범위의 맨 위로 끌어올리는 것처럼 보이는 현상

즉 함수 선언문이 마치 코드 맨 앞단에 작성된 것처럼 작동하는 현상을 의미한다.

선언문

hello() // hello

function hello() {
console.log('hello')
}

hello() // hello

위의 코드를 보면 함수 선언문 전에 hello() 함수를 실행해도 함수가 잘 실행되는 걸 볼 수 있다. 어떻게 이런일이 일어날 수 있는걸까?

바로 호이스팅이 일어났기 때문이다.

function hello() {
console.log('hello')
}

hello() // hello

hello() // hello

이런식으로 함수 표현식은 위로 끌어올려진 것처럼 작동한다.

실제 호이스팅이 일어나는 이유는 함수 자체가 미리 메모리에 등록하기 때문이다. 따라서 선언 전에 호출이 가능한 것 !

표현식

console.log(typeof hello === 'undefined') // true

hello() // Uncaught TypeError: hello is not a function

var hello = function () {
console.log('hello')
}

hello() // hello

함수 표현식은 함수 선언문과 다르게 정상적으로 호출되지 않고 undefined로 남아있는 것을 볼 수 있다.

//var hello;  // 선언만 호이스팅되어 undefined로 초기화됨

hello; // undefind => 식별자는 이미 존재함 (위로 호이스팅 되었기에)
hello(); // 함수 호출 불가 => hello가 아직 undefined 상태이기 때문

var hello = function () {
console.log('hello')
}

hello(); //hello

함수 표현식은 호이스팅이 일어나지 않는 걸 볼 수 있다.
함수와 다르게 변수는 런타임 이전에 undefined로 초기화되고 할당문이 실행되는 시점, 즉 런타임 시점에 함수가 할당되어 작동하기 때문이다.


좋은 함수 작성 원칙

함수의 부수 효과 최소화

함수의 부수 효과란

함수 내의 작동으로 인해 함수가 아닌 함수 외부에 영향을 끼치는 것을 의

이러한 부수 효과가 없는 함수를 순수 함수라 하고 부수 효과가 존재하는 함수를 비순수 함수라고 한다.

부수효과들

상황부수 효과의 예이유
데이터 요청fetch()서버에 요청(외부 영향)
콘솔 출력console.log()브라우저 콘솔 조작
문서 제목 변경document.title = ...DOM 조작(외부 변경)
setTimeout브라우저 타이머에 영향외부 환경 의존

부수 효과를 완전히 없앨 수는 없고 제한된 곳에서만 안전하게 처리해야 한다는 게 핵심 !

리액트에서 부수 효과를 다루는 법 → useEffect

리액트는 useEffect를 통해 이런 부수 효과를 제어된 방식으로 실행

function Example({ userId }) {
  const [data, setData] = useState(null);

  // 외부 요청(부수 효과)은 useEffect 내부에서만 수행
  useEffect(() => {
    fetch(`/api/user/${userId}`)
      .then(res => res.json())
      .then(setData);
  }, [userId]);

  return <div>{data?.name}</div>;
}

핵심 포인트:

  • 렌더링은 순수하게 유지
  • 외부 동작(데이터 요청 등)은 useEffect 내부에서만 수행
  • 이렇게 하면 렌더링 로직과 부수 효과 로직이 명확히 분리

하지만 useEffect는 DOM을 생성 후 paint 이후에 동작하기 때문에 렌더링에 영향을 주는 로직에서는 사용하면 X !!

상황예시이유
1. 외부 데이터 요청 / API 호출fetch, axios, supabase, firebase네트워크 I/O는 렌더링과 독립적
2. DOM 직접 조작 (Ref 기반)element.focus(), scrollIntoView()리액트가 관리하지 않는 영역
3. 타이머/인터벌 관리setTimeout, setInterval브라우저 자원과의 상호작용
4. 구독/리스너 등록 및 해제addEventListener, WebSocket, Firebase 등컴포넌트가 “외부 이벤트”를 듣는 행위

위와 같이 렌더링에 영향을 안주고 외부 세계에 영향을 주거나 의존하는 코드를 처리할 때만 사용해야한다.

부수 효과를 최소화해야하는 이유

이유설명
예측 가능성같은 입력에 대해 결과가 일정해야 버그 추적이 쉬움
테스트 용이성순수 함수는 테스트하기 쉽고 빠름
컴포넌트 안정성불필요한 재렌더링, 상태 꼬임 방지
협업과 유지보수다른 개발자가 읽고 이해하기 쉬움

가능한 한 함수를 작게 만들기

함수는 한 가지 일만 해야 한다. 한 함수에 여러 기능들이 존재한다면 유지보수성도 어렵고 가독성도 좋지 않고 재사용에 있어서도 좋지 않다.

따라서 단일책임 원칙을 따라야 한다.

ESLint의 max-lines-per-function 규칙은 하나의 함수가 너무 길면 경고를 띄워라라는 규칙이다.

"max-lines-per-function": ["warn", { "max": 50 }]

기본 ESLint 규칙에 있을 정도로 생각하면서 작성해야 할 부분이다.

함수 길이의 적당함 ?

상황에 따라 다르지만, 짧을수록 좋다

하지만 실무에서는 보통 20줄 이내면 읽기 편하고 10줄 내외면 가장 이상적이다.

하지만 한 함수에 하나의 기능을 하지만 코드가 길어질 수 있기 때문에
무조건 짧아야 한다는건 잘못된 생각이다 !!

문제해결 방법
한 함수가 여러 일을 함작은 함수로 분리
비슷한 로직 반복공통 함수 추출
긴 조건문의미 있는 변수/함수로 치환
중첩 콜백 많음async/await, map/filter 등 활용

나쁜 예시

function processOrder(order) {
  if (!order.user) return;
  if (!order.items.length) return;

  // 재고 확인
  for (const item of order.items) {
    if (item.stock === 0) return;
  }

  // 결제 처리
  const result = pay(order.total);
  if (!result.success) return;

  // 알림 전송
  sendEmail(order.user.email);
  sendSMS(order.user.phone);
}

좋은 예시

function validateOrder(order) { /* ... */ }
function checkStock(items) { /* ... */ }
function processPayment(order) { /* ... */ }
function notifyUser(user) { /* ... */ }

function processOrder(order) {
  if (!validateOrder(order)) return;
  if (!checkStock(order.items)) return;
  if (!processPayment(order)) return;
  notifyUser(order.user);
}

누구나 이해할 수 있는 함수명

작은 프로젝트는 코드의 양이 적고, 작성자 본인만 이해하면 충분하기 때문에 temp, data, result 같은 이름을 써도 큰 문제가 없다.
하지만 프로젝트가 커지고 기능이 복잡해지며 여러 사람이 함께 작업하게 되면
이름이 불분명한 변수나 함수는 가독성을 떨어뜨리고 유지보수를 어렵게 만든다.

좋은 네이밍의 조건

이름만 보고 역할이 드러나는 코드

// ❌ 나쁜 예
function processData(a, b) {
  return a * b;
}

// ✅ 좋은 예
function calculateInsuranceFee(insurance, years) {
  return insurance * years;
}

좋은 이름을 짓기 위한 세 가지 원칙

원칙설명
명확하게함수 이름만 보고 “무슨 일을 하는지” 알 수 있어야 한다.
일관성 있게프로젝트 내 동일한 역할의 함수는 동일한 패턴으로 이름을 짓는다. 예: handleClick, handleSubmit, fetchData
읽기 쉽게약어나 축약어는 피하고, 가능한 한 자연어처럼 읽히게 작성한다.
// ✅ 좋은 패턴
handleSubmit();     // 동작 수행
fetchUserData();    // 외부 요청
calculatePrice();   // 계산
getUserInfo();      // 반환

💡 클로저

리액트에서 함수 컴포넌트와 훅이 등장한 16.8 버전을 기점으로 이 클로저라는 개념이 리액트에서 적극적으로 사용되기 시작하면서 클로저를 빼놓고서는 리액트가 어떤 식으로 작동하는지 이해할 수 없다.

클로저의 정의

MDN 공식문서에서 말하는 클로저 정의

함수와 함수가 선언된 어휘적 환경(Lexical Scope)의 조합

렉시컬 스코프 (어휘적 환경)

스코프: 변수의 유효 범위

function add() {
  const a = 10
  function innerAdd() {
    const b = 20
    console.log(a + b)
  }
  innerAdd() // 30
}

add()
  • a 변수의 유효 범위는 add 전체
  • b 변수의 유효 범위는 innderAdd 전체
    => innerAdd는 add 내부에서 선언돼 있어 a를 사용 가능!

즉 렉시컬 스코프는 변수가 코드 내부에서 어디서 선언됐는지를 말하는 것

전역 스코프

전역스코프: 변수를 전역 레벨에 선언하는 것

var global = 'global scope'

function heUo() {
  console.log(global)
}
console.log(global) // global scope

hello() // global scope

console.log(global === window.global) // true
  • global이라는 변수를 var와 함께 선언했더니 전역 스코프hello 스크프 모두에서 global 변수에 접근 가능

함수 스코프

자바스크립트는 기본적으로 함수 레벨 스코프를 따른다. 즉, {} 블록이 스코프 범위를 결정하지 X

if (true) {
  var global = 'global scope'
}

console.log(global) // 'global scope'
console.log(global === window.global) // true
  • {} 내부에서 선언돼 있는데, {} 밖에서도 접근이 가능
    => 기본적으로 자바스크립트는 함수 레벨 스코프를 가지고 있기 때문
function hello() {
  var local = 'local variable'
  console.log(local) // local variable
}

hello()
console.log(local) // Uncaught ReferenceError: local is not defined
  • 단순한 if 블록과는 다르게 함수 블록 내부에서는 일반적으로 예측하는 것과 같이 스코프가 결정

리액트에서의 클로저

function Component() {
  const [count, setCount] = useState(0);

  function handleClick() {
    setCount((prev) => prev + 1);
  }
}
  • 이때 useState는 한 번 실행되고 끝났는데 이후에 setCount는 어떻게 계속 최신 count 값을 알고 있을까?
    => 바로 클로저 때문

setCount 함수는 useState 내부에서 만들어질 때
count가 저장된 환경(Lexical Environment) 을 함께 기억한다.

그래서 나중에 setCount를 호출해도 useState 실행이 끝난 이후임에도 불구하고 그 당시의 state 변수에 접근할 수 있다.

즉, useState는 내부적으로 클로저를 이용한 상태 은닉 구조를 사용한다.

클로저 주의사항

클로저는 유용하지만 성능이나 메모리에 영향을 줄 수 있다.

1. 잘못된 스코프 바인딩

for (var i = 0; i < 5; i++) {
  setTimeout(function () {
    console.log(i);
  }, i * 1000);
}

의도: 1초 간격으로 0, 1, 2, 3, 4 출력
결과: 전부 5만 출력

이유:

  • var는 함수 레벨 스코프라서 for 루프가 끝난 시점에는 전역 i = 5만 남음
  • 각 setTimeout은 모두 같은 i(=5) 를 참조

2. 메모리 낭비

function heavyJobWithClosure() {
  const longArr = Array.from({ length: 10000000 }, (_, i) => i + 1);

  return function () {
    console.log(longArr.length);
  };
}

const innerFunc = heavyJobWithClosure();
button.addEventListener('click', innerFunc);
  • innerFunc가 longArr를 클로저로 기억
  • 브라우저는 longArr를 GC(가비지 컬렉션) 하지 못함

클로저의 단점 정리

문제원인해결 방법
메모리 점유외부 변수를 계속 기억함클로저 내부에서 필요한 최소한의 값만 유지
예기치 않은 참조var로 선언된 전역 스코프 공유let / const 사용
디버깅 어려움내부 상태가 외부에서 안 보임주석, 명확한 함수 이름으로 보완

3개의 댓글

comment-user-thumbnail
2025년 11월 8일

커스텀훅을 구현하면서 줄이 너무 길어지고 가독성이 안 좋아져서 고민이 많았는데 좋은 함수 작성 원칙 덕에 도움이 많이 됐습니다!! 커스텀 훅을 조금 더 기능 별로 세세하게 나누고 알맞는 네이밍을 하면 코드가 더 좋아질 것 같아요

함수의 부수효과를 최소화하기 위해 useEffect 사용을 보여주셨는데, 덕분에 useEffect를 어떤 용도로 사용할 수 있을지 명확히 알게 되었네요.

답글 달기
comment-user-thumbnail
2025년 11월 9일

undefined vs null 비교를 생성 방식부터 직렬화까지 구체적으로 정리한 점이 좋았어요.
shallowEqual 한계 → memo 실패 사례(객체 리터럴 참조 변경) 흐름을 정리해주셔서 이해가 잘 되었고, 대응 전략을 useMemo/useCallback으로 연결한 것도 도움이 많이 되었습니다 ..!! 🙂

memo 컴포넌트에 객체 props를 넘길 때, 참조 안정화를 위해 useMemo를 쓰는 기준은 어떻게 잡으시나요? (빈도, 비용, 구조 중 어떤 걸 우선 고려하시는지 궁금합니다!)

답글 달기
comment-user-thumbnail
2025년 11월 9일

함수 작성 원칙 부분에서 예시까지 보여주셔서 좋았습니다. 커스텀 훅을 작성할 때 한 함수 안에 로직이 길어질 때마다 한 가지 일만 하는 함수인가? 를 스스로 계속 점검해야겠다는 생각이 들었습니다.
이 밖에도 여러 예시를 보여주며 설명해주시는 점이 인상적이었습니다.

답글 달기