이 내용은 nginx를 쓰고 있는 가상서버에서는 공통적으로 가능한 ssl 세팅입니다.

1. SSL 인증서 파일 준비하기

일반적인 웹사이트의 경우, SSL 인증서를 구매하거나 Let's Encrypt 같은 무료 서비스에서 발급받아 다음과 같은 파일들을 준비합니다:

  • ssl.crt 또는 certificate.crt: SSL 인증서 파일
  • ssl.key 또는 private.key: 프라이빗 키 파일

 

2. 필요한 디렉토리 생성하기

# 해당하는 폴더를 만듭니다.
sudo mkdir -p /etc/ssl/certs
sudo mkdir -p /etc/ssl/private

 

3. 인증서와 키 파일 복사 및 권한 설정

# 준비된 crt와 key 파일에 대한 권한을 설정합니다.
# 보통 filezilla를 통해 파일을 업로드한 뒤 작업합니다.
sudo chmod 644 /etc/ssl/certs/ssl.crt
sudo chmod 600 /etc/ssl/private/ssl.key

 

4. 암호화된 키 파일 처리하기

키 파일에 암호가 설정되어 있다면, 서버 재시작시 자동으로 SSL이 적용되도록 암호를 제거합니다:

# 원본 키 파일 백업
cp /etc/ssl/private/ssl.key /etc/ssl/private/ssl.key.orig

# 암호 없는 키 파일로 변환
openssl rsa -in /etc/ssl/private/ssl.key.orig -out /etc/ssl/private/ssl.key

# 적절한 권한 다시 설정
chmod 600 /etc/ssl/private/ssl.key

 

5. Nginx 설정 파일 수정하기

웹사이트의 설정 파일을 수정합니다:

# nginx의 경우 sites-available 에서 http, https 등의 블록을 위해 파일을 손봐야합니다.
# 파일의 경로가 꼭 이렇진 않습니다. sites-available만 참고해주세요
vim /etc/nginx/sites-available/{YOUR-WEBSITE}

- 일반적인 웹사이트의 Nginx 설정 예시:

# HTTP 설정 - HTTPS로 리다이렉트
server {
    listen 80;
    server_name example.com www.example.com;
    
    # HTTPS로 리다이렉트
    location / {
        return 301 https://$host$request_uri;
    }
}

# HTTPS 설정
server {
    listen 443 ssl;
    server_name example.com www.example.com;

    # SSL 인증서 설정
    ssl_certificate /etc/ssl/certs/ssl.crt;
    ssl_certificate_key /etc/ssl/private/ssl.key;

    # SSL 프로토콜 설정
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;
    ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384;
    
    # 정적 파일이 있는 루트 디렉토리 설정
    root /var/www/html;
    index index.html index.htm index.php;
    
    # 기본 위치 설정
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }
    
    # PHP 처리 설정 (PHP 사용 시)
    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/var/run/php/php7.4-fpm.sock; # PHP 버전에 맞게 수정
    }
    
    # 접근 로그와 에러 로그 위치
    access_log /var/log/nginx/example.com-access.log;
    error_log /var/log/nginx/example.com-error.log;
}

 

6. Nginx 설정 테스트 및 재시작

# 설정이 올바른지 테스트
nginx -t

# 설정이 올바르면 Nginx 재시작
systemctl restart nginx

 

7. 방화벽 설정 확인 (필요한 경우)

# UFW 방화벽 사용 시, HTTPS 포트 개방
sudo ufw allow 443/tcp

 

8. 확인 및 문제 해결

웹 브라우저에서 https://example.com으로 접속하여 SSL이 제대로 적용되었는지 확인합니다. 주소 표시줄에 자물쇠 아이콘이 표시되면 성공적으로 적용된 것입니다.

문제가 발생한 경우 다음 로그 파일을 확인합니다:

# Nginx 오류 로그 확인
tail -n 100 /var/log/nginx/error.log

 

이런 과정으로 가상서버에서 호스팅하는 웹사이트에 SSL을 적용하는 과정이 완료되었습니다.

가비아나 AWS에서는 간편하게 설정할 수 있었고, 자동으로 적용이 되었던 부분인데

SSL을 신청 받고 /.well-known/pki-validation/ 에 http 인증용 파일을 올렸는데도 발급이 안돼서 무슨 문제인가 했어요

다른 분들에게도 도움이 되었으면 좋겠습니다

이전 포스팅 https://ipjaworld.tistory.com/63 에서 이어지는 내용입니다.

포스팅은 3편에 나눠서 업로드 됩니다.

A. 자바스크립트 조망하기
1. 강제변환과 깊은비교, 일치비교와 동등비교,
2. js의 this 키워드와 프로토타입, 그리고 메모리주소

B. 추가 주제
1. es6 모듈에서 class를 사용하지 않는 이유

 

B. 추가 주제

1. ES6 모듈에서 class를 사용하지 않는 이유

ES6(ECMAScript 2015)에서는 클래스 문법과 모듈 시스템이 동시에 도입되었다. 그러나 "You Don't Know JS Yet"을 읽으면서 흥미로웠던 점은 많은 자바스크립트 개발자들, 특히 함수형 프로그래밍을 선호하는 개발자들이 ES6 모듈에서 클래스 사용을 지양한다는 것이다. 왜 그럴까?

클래스의 겉모습과 실제 동작의 차이

자바스크립트의 클래스는 다른 언어(Java, C++ 등)의 클래스와 문법적으로 유사하지만, 내부적으로는 여전히 프로토타입 기반으로 동작한다. 이런 "문법적 설탕(syntactic sugar)"은 자바스크립트의 객체 지향 특성을 이해하지 못한 채 다른 언어의 패러다임을 그대로 가져오게 만든다.

// 클래스 문법
class Person {
  constructor(name) {
    this.name = name;
  }
  
  greet() {
    return `Hello, I'm ${this.name}`;
  }
}

// 위 코드는 내부적으로 다음과 같이 동작한다
function Person(name) {
  this.name = name;
}

Person.prototype.greet = function() {
  return `Hello, I'm ${this.name}`;
};

this 바인딩 문제

클래스 메서드를 이벤트 핸들러나 콜백으로 전달할 때 this 바인딩 문제가 자주 발생한다.

class Counter {
  constructor() {
    this.count = 0;
    this.button = document.getElementById('button');
    // this 바인딩을 수동으로 처리해야 함
    this.button.addEventListener('click', this.increment.bind(this));
  }
  
  increment() {
    this.count++;
    console.log(this.count);
  }
}

이런 코드는 this 바인딩을 수동으로 처리해야 하므로 실수하기 쉽고, 코드의 복잡성이 증가한다.

상태 관리와 불변성

함수형 프로그래밍에서는 상태 변경을 최소화하고 불변성(immutability)을 중요시한다. 클래스는 본질적으로 내부 상태를 가지며, 메서드를 통해 그 상태를 변경하는 방식으로 동작한다.

// 클래스 기반 접근법 (상태 변경)
class Cart {
  constructor() {
    this.items = [];
  }
  
  addItem(item) {
    this.items.push(item);  // 내부 상태 변경
  }
  
  getTotal() {
    return this.items.reduce((total, item) => total + item.price, 0);
  }
}

// 함수형 접근법 (불변성 유지)
const createCart = (items = []) => ({
  items,
  addItem: (item) => createCart([...items, item]),  // 새 객체 반환
  getTotal: () => items.reduce((total, item) => total + item.price, 0)
});

함수형 접근법은 원본 데이터를 변경하지 않고 새로운 상태를 반환하므로, 예측 가능성이 높아지고 부작용(side effects)이 감소한다.

모듈 패턴의 유연성

ES6 모듈은 그 자체로 격리된 스코프를 제공하므로, 클래스 없이도 캡슐화를 쉽게 구현할 수 있다.

// counter.js
let count = 0;  // 모듈 내부에서만 접근 가능한 private 상태

export const increment = () => ++count;
export const decrement = () => --count;
export const getCount = () => count;

// 사용
import { increment, getCount } from './counter.js';
increment();
console.log(getCount());  // 1

이 패턴은 클래스보다 더 간결하고, 필요한 기능만 노출할 수 있어 API 설계가 명확해진다.

상속보다 합성

클래스 상속은 종종 "깨지기 쉬운 기반 클래스(fragile base class)" 문제를 일으킨다. 부모 클래스의 변경이 예기치 않게 자식 클래스에 영향을 미칠 수 있기 때문이다.

// 상속 기반 접근법
class Animal {
  constructor(name) {
    this.name = name;
  }
  
  speak() {
    console.log(`${this.name} makes a noise.`);
  }
}

class Dog extends Animal {
  speak() {
    console.log(`${this.name} barks.`);
  }
}

// 합성 기반 접근법
const createAnimal = (name) => ({
  name,
  speak: () => console.log(`${name} makes a noise.`)
});

const createDog = (name) => ({
  ...createAnimal(name),
  speak: () => console.log(`${name} barks.`)
});

합성(composition)은 "has-a" 관계를 표현하며, 상속보다 더 유연하고 느슨한 결합을 제공한다.

테스트 용이성

함수형 접근법은 일반적으로 테스트하기 더 쉽다. 순수 함수(pure functions)는 동일한 입력에 대해 항상 동일한 출력을 반환하며, 외부 상태에 의존하지 않는다.

// 클래스 메서드 테스트 (상태에 의존)
test('Cart.addItem', () => {
  const cart = new Cart();
  cart.addItem({ name: 'Product', price: 10 });
  expect(cart.items.length).toBe(1);
});

// 함수형 접근법 테스트 (상태 독립적)
test('addItem', () => {
  const cart = createCart();
  const newCart = cart.addItem({ name: 'Product', price: 10 });
  expect(newCart.items.length).toBe(1);
  expect(cart.items.length).toBe(0);  // 원본은 변경되지 않음
});

클래스 vs 함수형 패턴 비교표

아래 표는 클래스 기반 접근법과 함수형 패턴의 주요 차이점을 비교한 것이다:

특성 클래스 기반 접근법 함수형 패턴

상태 관리 내부 상태를 변경 (mutable) 불변 상태 (immutable) 지향
코드 구조 객체와 메서드 중심 함수와 데이터 중심
상속 방식 클래스 상속 (is-a 관계) 객체 합성 (has-a 관계)
this 처리 컨텍스트 관리 필요 컨텍스트 의존성 최소화
부작용 객체 내부 상태 변경 흔함 부작용 최소화 지향
테스트 용이성 상태 의존적, 복잡할 수 있음 순수 함수로 테스트 용이
메모리 사용 인스턴스마다 메모리 할당 함수 공유, 효율적 메모리 사용 가능
디버깅 상태 추적이 어려울 수 있음 데이터 흐름 추적 용이
확장성 상속을 통한 확장 합성을 통한 확장
가독성 객체지향 개발자에게 친숙 함수형 패러다임 이해 필요

실제 프레임워크 사례: React의 함수형 컴포넌트 전환

React는 초기에 클래스 컴포넌트를 주로 사용했지만, 16.8 버전에서 Hooks가 도입되면서 함수형 컴포넌트로의 전환이 가속화되었다. 이는 프론트엔드 개발에서 함수형 접근법의 장점을 잘 보여주는 사례다.

// 클래스 컴포넌트
class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
    this.increment = this.increment.bind(this);
  }

  increment() {
    this.setState({ count: this.state.count + 1 });
  }

  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={this.increment}>Increment</button>
      </div>
    );
  }
}

// 함수형 컴포넌트 + Hooks
function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

함수형 컴포넌트는 더 간결하고, this 바인딩 문제가 없으며, Hooks를 통해 상태 관리와 생명주기 기능을 필요한 만큼만 사용할 수 있다. React 팀은 공식적으로 함수형 컴포넌트와 Hooks 사용을 권장하고 있으며, 이는 자바스크립트 생태계에서 함수형 프로그래밍의 영향력을 보여준다.

또한 React의 state 업데이트 철학은 불변성에 기반하며, 이는 함수형 프로그래밍의 핵심 원칙과 일치한다:

// 권장되지 않는 방식 (직접 상태 변경)
const handleClick = () => {
  const newItems = this.state.items;
  newItems.push('New Item');
  this.setState({ items: newItems }); // 잘못된 패턴
};

// 권장되는 방식 (불변성 유지)
const handleClick = () => {
  this.setState(prevState => ({
    items: [...prevState.items, 'New Item'] // 새 배열 생성
  }));
};

다른 라이브러리와 프레임워크에서의 함수형 접근

React 외에도 많은 현대적인 자바스크립트 라이브러리와 프레임워크들이 함수형 접근법을 채택하고 있다:

  1. Redux: 예측 가능한 상태 관리를 위해 순수 함수인 리듀서(reducer)와 불변성 원칙을 따른다.
  2. Vue 3: Composition API를 도입하여 함수형 접근법을 지원한다.
  3. Svelte: 선언적 템플릿과 반응형 상태를 사용하며, 클래스 대신 함수형 패러다임을 채택한다.
  4. Functional CSS Libraries (Tailwind, Tachyons): 작은 단일 기능 클래스를 합성하여 스타일을 구성하는 방식은 합성 개념과 일치한다.

성능 고려사항

클래스 인스턴스 생성은 프로토타입 체인 설정과 new 연산자 오버헤드가 있다. 단순 객체나 클로저를 사용하는 팩토리 함수는 경우에 따라 더 효율적일 수 있다.

물론 현대 자바스크립트 엔진은 최적화가 잘 되어 있어 대부분의 경우 성능 차이는 미미하지만, 성능이 중요한 애플리케이션에서는 고려할 만한 요소다.

결론: 상황에 맞는 도구 선택하기

클래스가 항상 나쁜 것은 아니다. 특히 다음과 같은 경우에는 클래스가 적합할 수 있다:

  1. 객체 지향 설계가 자연스러운 도메인(예: UI 컴포넌트)
  2. 상속이 명확하게 "is-a" 관계를 표현할 때
  3. 팀이 클래스 기반 패러다임에 더 익숙한 경우
  4. 기존 클래스 기반 라이브러리/프레임워크와 통합해야 할 경우

하지만 모듈 시스템을 사용할 때는 항상 가장 단순하고 명확한 방법을 선택하는 것이 좋다. 대부분의 경우, 간단한 팩토리 함수나 객체 리터럴, 또는 순수 함수의 집합으로도 충분하다.

"You Don't Know JS Yet"에서 배운 가장 중요한 것 중 하나는 자바스크립트의 유연성을 이해하고, 특정 패러다임에 얽매이지 않고 상황에 가장 적합한 도구를 선택하는 것이다. 클래스가 필요하다면 사용하되, 단순히 다른 언어의 패턴을 모방하기 위해 사용하는 것은 피하자.

이전 포스팅 https://ipjaworld.tistory.com/63 에서 이어지는 내용입니다.

포스팅은 3편에 나눠서 업로드 됩니다.

A. 자바스크립트 조망하기
1. 강제변환과 깊은비교, 일치비교와 동등비교,
2. js의 this 키워드와 프로토타입, 그리고 메모리주소

B. 추가 주제
1. es6 모듈에서 class를 사용하지 않는 이유

 

A. 자바스크립트 조망하기

2. js의 this 키워드와 프로토타입, 그리고 메모리주소

"You Don't Know JS Yet"을 읽기 전까지 자바스크립트의 this 키워드와 프로토타입 시스템은 나에게 미지의 영역과 같았다. 특히 다른 언어에서의 this와 다르게 동작하는 자바스크립트의 this는 많은 혼란을 가져왔다. 이번에는 this와 프로토타입, 그리고 자바스크립트의 메모리 주소 관리에 대해 내가 새롭게 이해한 내용을 정리해보려 한다.

this 키워드: 호출 시점에 결정되는 컨텍스트

자바스크립트의 this는 다른 언어와 달리 함수가 호출되는 방식에 따라 결정된다. 이것은 내가 기존에 알고 있던 객체 지향 언어(예: Java, C++)의 this와 완전히 다른 개념이다.

가장 헷갈렸던 코드 예시를 살펴보자:

const person = {
  name: "Alice",
  greet: function() {
    console.log(`Hello, my name is ${this.name}`);
  }
};

person.greet();  // "Hello, my name is Alice"

const greetFunction = person.greet;
greetFunction();  // "Hello, my name is undefined"

왜 같은 함수를 호출했는데 다른 결과가 나올까? 그것은 this가 함수를 어떻게 호출했느냐에 따라 결정되기 때문이다.

this 바인딩 규칙

자바스크립트에서 this는 다음 규칙에 따라 결정된다(우선순위 순):

  1. 명시적 바인딩: call, apply, bind 메서드를 사용해 this를 명시적으로 지정할 수 있다.
  2. greetFunction.call(person); // "Hello, my name is Alice"
  3. new 바인딩: 생성자 함수를 new 키워드로 호출하면 this는 새로 생성된 객체를 가리킨다.
  4. function Person(name) { this.name = name; } const alice = new Person("Alice"); // this는 새로 생성된 객체를 가리킴
  5. 암시적 바인딩: 객체의 메서드로 호출되면 this는 그 객체를 가리킨다.
  6. person.greet(); // this는 person을 가리킴
  7. 기본 바인딩: 일반 함수 호출 시 this는 전역 객체(브라우저에서는 window, Node.js에서는 global)를 가리킨다. 엄격 모드('use strict')에서는 undefined가 된다.
  8. function showThis() { console.log(this); } showThis(); // window 또는 global (또는 엄격 모드에서는 undefined)
  9. 화살표 함수: 화살표 함수는 자신만의 this를 갖지 않고, 외부 스코프의 this를 그대로 사용한다.화살표 함수는 선언될 때의 외부 스코프 this를 유지하므로, 객체 메서드나 이벤트 핸들러에서 원래 객체의 컨텍스트를 유지하고 싶을 때 유용하다:
  10. const counter = { count: 0, increase: function() { // 화살표 함수는 counter의 this를 유지 setInterval(() => { console.log(++this.count); // counter.count를 증가 }, 1000); } }; counter.increase();
  11. const person = { name: "Alice" }; const normalFunction = function() { return this.name; }; const arrowFunction = () => this.name; console.log(normalFunction.call(person)); // "Alice" console.log(arrowFunction.call(person)); // undefined (또는 전역 객체의 name)

이 규칙들을 이해하기 전에는 this의 동작이 너무 혼란스러웠다. 특히 콜백 함수에서 this가 예상과 다르게 동작하는 경우가 많았는데, ES6의 화살표 함수는 이런 문제를 해결하는 데 큰 도움이 되었다.

프로토타입: 자바스크립트의 상속 메커니즘

자바스크립트의 프로토타입 시스템은 클래스 기반 언어와는 다른 방식으로 상속을 구현한다. 이를 "프로토타입 기반 상속"이라고 한다.

프로토타입 체인

모든 자바스크립트 객체는 내부 프로퍼티 [[Prototype]](또는 __proto__)를 갖고 있으며, 이는 다른 객체를 가리킨다. 객체의 프로퍼티를 참조할 때 그 객체에 해당 프로퍼티가 없으면, 자바스크립트 엔진은 프로토타입 체인을 따라 프로퍼티를 찾는다.

const animal = {
  eat: function() {
    console.log("Eating...");
  }
};

const dog = Object.create(animal);
dog.bark = function() {
  console.log("Woof!");
};

dog.eat();  // "Eating..." - dog 객체에 없지만 프로토타입 체인을 통해 animal에서 찾음

이 코드에서 dog의 프로토타입은 animal 객체다. dog.eat()을 호출하면, dog 객체에 eat 메서드가 없기 때문에 프로토타입 체인을 따라 animal 객체에서 eat 메서드를 찾는다.

__proto__와 prototype의 차이점

자바스크립트에서 __proto__와 prototype은 다른 개념이다:

  • __proto__: 모든 객체가 가지는 내부 프로퍼티로, 해당 객체의 프로토타입을 가리킨다.
  • prototype: 함수 객체만 가지는 프로퍼티로, 해당 함수를 생성자로 사용했을 때 만들어지는 객체의 프로토타입이 된다.
function Dog(name) {
  this.name = name;
}

Dog.prototype.bark = function() {
  console.log(`${this.name} says woof!`);
};

const rex = new Dog("Rex");

console.log(rex.__proto__ === Dog.prototype);  // true
console.log(Dog.__proto__ === Function.prototype);  // true

생성자 함수와 prototype 속성

함수는 특별한 prototype 속성을 갖는데, 이는 해당 함수를 생성자로 사용해 만든 객체의 프로토타입이 된다.

function Person(name) {
  this.name = name;
}

Person.prototype.greet = function() {
  console.log(`Hello, my name is ${this.name}`);
};

const alice = new Person("Alice");
alice.greet();  // "Hello, my name is Alice"

이 코드에서 alice의 프로토타입은 Person.prototype이다. 따라서 alice 객체는 Person.prototype에 정의된 greet 메서드에 접근할 수 있다.

클래스 문법과 프로토타입

ES6에서 도입된 class 문법은 프로토타입 기반 상속을 더 쉽고 명확하게 사용할 수 있게 해준다. 하지만 내부적으로는 여전히 프로토타입을 사용한다.

class Animal {
  constructor(name) {
    this.name = name;
  }
  
  eat() {
    console.log(`${this.name} is eating.`);
  }
}

class Dog extends Animal {
  bark() {
    console.log(`${this.name} says woof!`);
  }
}

const rex = new Dog("Rex");
rex.eat();  // "Rex is eating."
rex.bark();  // "Rex says woof!"

이 코드에서 rex의 프로토타입 체인은 Dog.prototype -> Animal.prototype -> Object.prototype이다.

클로저와 프로토타입의 관계

클로저는 자바스크립트의 또 다른 강력한 개념이다. 클로저는 함수와 그 함수가 선언된 렉시컬 환경의 조합으로, 내부 함수가 외부 함수의 변수에 접근할 수 있게 해준다.

프로토타입과 클로저를 함께 사용하면 강력한 패턴을 만들 수 있다:

function createCounter() {
  let count = 0;  // 클로저를 통해 접근 가능한 private 변수
  
  function Counter() {}
  
  Counter.prototype.increment = function() {
    count++;
    return count;
  };
  
  Counter.prototype.decrement = function() {
    count--;
    return count;
  };
  
  return new Counter();
}

const counter = createCounter();
console.log(counter.increment());  // 1
console.log(counter.increment());  // 2
console.log(counter.decrement());  // 1

이 예제에서 count 변수는 클로저를 통해 Counter 프로토타입의 메서드들이 접근할 수 있지만, 외부에서는 직접 접근할 수 없는 private 변수처럼 동작한다.

메모리 주소와 가비지 컬렉션

자바스크립트에서 변수의 메모리 관리는 대부분 자동으로 이루어진다. 하지만 그 동작 원리를 이해하는 것은 효율적인 코드를 작성하는 데 큰 도움이 된다.

원시 타입과 참조 타입의 메모리 관리

  1. 원시 타입: 원시 타입(string, number, boolean, null, undefined, symbol, bigint)은 값 자체가 변수에 저장된다. 변수에 다른 원시 값을 할당하면 이전 값은 덮어써진다.
  2. 참조 타입: 참조 타입(object, array, function 등)은 실제 객체는 힙(heap) 메모리에 저장되고, 변수에는 그 객체의 메모리 주소가 저장된다.
let x = { name: "John" };
let y = x;  // y는 x와 같은 객체를 참조

y.name = "Jane";
console.log(x.name);  // "Jane" - x와 y는 같은 객체를 참조하기 때문

가비지 컬렉션

자바스크립트는 자동으로 더 이상 참조되지 않는 객체를 메모리에서 해제하는 가비지 컬렉션을 수행한다. 이를 "도달 가능성(reachability)" 알고리즘이라고 한다.

let obj = { data: "some data" };
// 여기서 obj는 객체를 참조

obj = null;  
// obj가 더 이상 객체를 참조하지 않으므로, 그 객체는 가비지 컬렉션의 대상이 됨

클로저와 메모리 누수

클로저는 함수가 외부 환경의 변수를 참조할 수 있게 해주는데, 이로 인해 의도치 않은 메모리 누수가 발생할 수 있다.

function setupHandler() {
  let element = document.getElementById("button");
  let counter = 0;
  
  element.addEventListener("click", function() {
    console.log(++counter);
  });
}

이 코드에서 클로저(이벤트 핸들러 함수)는 counter 변수를 참조하고 있다. 만약 이벤트 리스너를 제거하지 않으면, counter 변수는 가비지 컬렉션의 대상이 되지 않아 메모리 누수가 발생할 수 있다.

메모리 누수를 방지하려면 다음과 같이 이벤트 리스너를 제거해야 한다:

function setupHandler() {
  let element = document.getElementById("button");
  let counter = 0;
  
  const clickHandler = function() {
    console.log(++counter);
  };
  
  element.addEventListener("click", clickHandler);
  
  // 나중에 이벤트 리스너 제거
  return function cleanup() {
    element.removeEventListener("click", clickHandler);
    element = null; // DOM 요소에 대한 참조도 제거
  };
}

const cleanup = setupHandler();
// 필요 없어지면 cleanup 함수 호출
// cleanup();

이터러블과 메모리 효율성

ES6에서 도입된 이터러블(iterable)과 제너레이터(generator)는 메모리 효율적인 데이터 처리를 가능하게 한다.

function* infiniteSequence() {
  let i = 0;
  while (true) {
    yield i++;
  }
}

const iterator = infiniteSequence();
console.log(iterator.next().value);  // 0
console.log(iterator.next().value);  // 1

이 제너레이터 함수는 무한한 숫자 시퀀스를 생성하지만, 모든 값을 메모리에 저장하지 않고 필요할 때만 값을 생성한다. 이는 대용량 데이터를 처리할 때 매우 유용하다.

비동기 코드에서의 this 바인딩

비동기 코드에서 this 바인딩은 특히 주의해야 할 부분이다. 콜백 함수는 원래 컨텍스트와 다른 컨텍스트에서 실행될 수 있기 때문이다.

const user = {
  name: "Alice",
  greetLater: function() {
    setTimeout(function() {
      console.log(`Hello, my name is ${this.name}`);
    }, 1000);
  }
};

user.greetLater();  // "Hello, my name is undefined"

이 문제는 화살표 함수나 bind 메서드를 사용하여 해결할 수 있다:

// 화살표 함수 사용
const user1 = {
  name: "Alice",
  greetLater: function() {
    setTimeout(() => {
      console.log(`Hello, my name is ${this.name}`);
    }, 1000);
  }
};

// bind 메서드 사용
const user2 = {
  name: "Bob",
  greetLater: function() {
    setTimeout(function() {
      console.log(`Hello, my name is ${this.name}`);
    }.bind(this), 1000);
  }
};

user1.greetLater();  // "Hello, my name is Alice"
user2.greetLater();  // "Hello, my name is Bob"

결론: 자바스크립트의 유연한 객체 시스템

자바스크립트의 this, 프로토타입, 메모리 관리는 처음에는 복잡하게 느껴질 수 있지만, 그 원리를 이해하면 매우 강력하고 유연한 시스템임을 알 수 있다.

  1. this 키워드는 호출 시점에 결정되며, 함수가 어떻게 호출되는지에 따라 달라진다.
  2. 프로토타입은 객체 간 상속 관계를 구현하는 자바스크립트만의 방식이다.
  3. 메모리 관리는 대부분 자동으로 이루어지지만, 참조 타입과 클로저를 사용할 때는 메모리 누수에 주의해야 한다.
  4. 클로저는 함수와 그 함수가 선언된 환경의 조합으로, 강력한 캡슐화를 가능하게 한다.
  5. 이터러블과 제너레이터는 메모리 효율적인 데이터 처리를 가능하게 한다.

이러한 개념들은 서로 밀접하게 연결되어 있으며, 자바스크립트의 특성을 이해하는 데 핵심이다. 특히 this와 프로토타입은 자바스크립트만의 독특한 개념이기 때문에, 다른 언어에서 온 개발자들이 자주 혼란을 겪는 부분이기도 하다.

하지만 이제 이런 개념들의 동작 원리를 더 명확하게 이해했으니, 자바스크립트의 강력한 기능들을 더 효과적으로 활용할 수 있을 것이다.

부제: 왜 [1,2,3] === [1,2,3]이 false인가? 자바스크립트 비교 연산 이해하기

you don't know JS Yet 을 읽고 난 뒤, 그 내용을 학습하기 위한 포스팅입니다. 
이 책은 자바스크립트 자체에 대한 내용, 즉 자바스크립트 이름의 유래와 명세서, JS의 하위 호환성과 상위 호환성에 대한 내용을 시작으로 이야기를 풀어갑니다. (이 부분에 대해서는 독후감, 독서활동이 아닌 추후에 더 공부해서 다른 프로그래밍 언어와 비교해서 정리한 비교 포스팅을 작성해볼 계획입니다.)
이 책에서 가장 인상 깊었던 것은 챕터별로 딱딱 나눠져서 있는 느낌이 아니라 js는 어떻게 발전했고 자바스크립트는 어떻게 사용되고 인식되어져 왔으며, 어떠한 배경에서 es6 모듈이 나왔고 지금의 JS가 있을 수 있었는지 이런 식으로 자연스럽게 이어지는 흐름이 대단했습니다. 오늘은 제가 잘못 이해하고 있었던 개념에 대해서 중심적으로 파보는 시간을 가져보려고 합니다. 제가 오늘 다룰 주제는 다음과 같습니다.

포스팅은 3편에 나눠서 업로드 됩니다.

A. 자바스크립트 조망하기
    1. 강제변환과 깊은비교, 일치비교와 동등비교,
    2. js의 this 키워드와 프로토타입, 그리고 메모리주소 

B. 추가 주제
    1. es6 모듈에서 class를 사용하지 않는

 

A. 자바스크립트 조망하기

1. 강제변환과 깊은비교, 일치비교와 동등비교

자바스크립트를 공부하면서 가장 헷갈렸던 부분 중 하나가 바로 비교 연산이었다. 특히 배열이나 객체를 비교할 때 예상과 다른 결과가 나와서 당황했던 경험이 있다. "You Don't Know JS Yet"을 읽으면서 내가 왜 이런 부분을 잘못 이해하고 있었는지 깨닫게 되었다.

원시 타입 vs 참조 타입

먼저 내가 가장 헷갈렸던 코드 예시를 살펴보자:

var x = [1, 2, 3];
var y = x;
var a = [1, 2, 3, 4];
var b = [1, 2, 3, 4];
var c = {a: 1, b: 2};
var d = {a: 1, b: 2};

console.log(y === x);  // true
console.log(a === b);  // false
console.log(c === d);  // false
console.log(y = [1, 2, 3]);  // [1, 2, 3] (할당 연산의 결과)
console.log(x === [1, 2, 3]);  // false

처음에는 단순히 "값이 같으면 같다고 판단하지 않나?"라고 생각했는데, 이는 자바스크립트의 참조 타입에 대한 이해가 부족했기 때문이다. 이제 제대로 정리해보자.

자바스크립트에서 데이터 타입은 크게 두 가지로 나눌 수 있다:

  1. 원시 타입(Primitive Types): string, number, boolean, null, undefined, symbol, bigint
  2. 참조 타입(Reference Types): object, array, function 등 (사실 이들은 모두 객체의 일종이다)

원시 타입은 값 자체가 변수에 할당되지만, 참조 타입은 메모리상의 주소값(참조)이 변수에 할당된다. 이 차이가 비교 연산에서 혼란을 일으키는 주요 원인이다.

 

일치 비교(===)와 동등 비교(==)

자바스크립트에서는 비교 연산자로 ===(일치 비교)와 ==(동등 비교)를 제공한다.

 

일치 비교(===)

일치 비교는 타입 변환 없이 값과 타입이 모두 같은지 비교한다. 참조 타입의 경우, 같은 객체를 참조하는지(즉, 같은 메모리 주소를 가리키는지) 확인한다.

var x = [1, 2, 3];
var y = x;  // y는 x와 같은 배열 객체를 참조
console.log(y === x);  // true: 같은 메모리 주소를 참조

var a = [1, 2, 3, 4];
var b = [1, 2, 3, 4];  // a와 b는 내용은 같지만 다른 배열 객체
console.log(a === b);  // false: 다른 메모리 주소를 참조

그래서 a === b가 false로 나오는 것이다! 내용이 같아도 서로 다른 객체(다른 메모리 주소)이기 때문이다.

 

동등 비교(==)

동등 비교는 타입이 다르면 타입 변환을 시도한 후 비교한다. 하지만 객체 간 비교에서는 ===와 마찬가지로 참조가 같은지만 확인한다.

console.log("1" == 1);  // true: 문자열 "1"이 숫자 1로 변환됨
console.log([1, 2, 3] == [1, 2, 3]);  // false: 다른 객체

 

강제 변환(Coercion)

자바스크립트의 또 다른 흥미로운 부분은 강제 변환이다. 강제 변환은 명시적(explicit)과 암묵적(implicit) 두 가지로 나뉜다.

 

명시적 강제 변환

개발자가 의도적으로 타입을 변환하는 경우다:

var a = "42";
var b = Number(a);  // 명시적으로 숫자로 변환
console.log(b);  // 42

 

암묵적 강제 변환

자바스크립트 엔진이 자동으로 타입을 변환하는 경우다:

var a = "42";
var b = a * 1;  // 암묵적으로 숫자로 변환
console.log(b);  // 42

console.log("10" > 9);  // true: "10"이 숫자 10으로 변환됨

여기서 내가 놀랐던 점은 비교 연산자 >=, <=, >, < 모두 ==와 같이 타입 변환을 수행한다는 것이다. 이런 암묵적 변환이 예상치 못한 결과를 가져올 수 있기 때문에 주의해야 한다.

 

특이 케이스: NaN

자바스크립트의 또 다른 특이한 점은 NaN(Not a Number)이다:

console.log(NaN === NaN);  // false

NaN은 자기 자신과도 같지 않다! 이는 IEEE 754 부동소수점 표준을 따르기 때문인데, NaN을 확인하려면 isNaN() 함수나 ES6의 Number.isNaN()을 사용해야 한다.

 

깊은 비교(Deep Equality)

객체나 배열의 내용이 같은지 비교하려면 어떻게 해야 할까? 이를 위해 "깊은 비교"를 수행해야 한다:

  1. JSON을 활용한 방법:단, 이 방법은 객체에 함수, 심볼, undefined 등이 포함되어 있거나 순환 참조가 있는 경우 제대로 작동하지 않는다.
  2. JSON.stringify(obj1) === JSON.stringify(obj2)
  3. lodash나 underscore 같은 라이브러리 사용:
  4. _.isEqual(obj1, obj2) // lodash의 isEqual 메서드
  5. 직접 재귀 함수 작성:
  6. function deepEqual(obj1, obj2) { // 타입이 다르면 바로 false 반환 if (typeof obj1 !== typeof obj2) return false; // 원시 타입이거나 null이면 일치 비교 if (obj1 === null || typeof obj1 !== 'object') return obj1 === obj2; // 키 개수 비교 const keys1 = Object.keys(obj1); const keys2 = Object.keys(obj2); if (keys1.length !== keys2.length) return false; // 각 키의 값 재귀적으로 비교 return keys1.every(key => deepEqual(obj1[key], obj2[key])); }

하지만 실무에서 객체 비교에 대해 더 고민하면서 깨달은 점이 있다. 사실 객체 전체를 깊게 비교하는 것이 항상 필요한 것은 아니다. 종종 우리가 정말로 필요한 것은 특정 프로퍼티나 필드의 값만 비교하는 것이다.

예를 들어, 사용자 목록에서 특정 ID를 가진 사용자를 찾는 경우:

const findUserById = (users, id) => users.find(user => user.id === id);

또는 상품 객체들 중에서 가격이 특정 범위에 있는 것만 필터링하는 경우:

const affordableProducts = products.filter(product => product.price < 50);

이렇게 명확한 비교 기준(id, price 등)을 정하고 범위를 좁히는 접근법이 실제로 더 효율적이고 가독성도 좋다. 모든 필드를 일일이 비교하는 깊은 비교는 성능 비용이 크고, 실제로는 그런 완전한 비교가 필요한 경우가 생각보다 많지 않다는 것을 알게 되었다.

물론 두 객체가 정확히 같은 상태인지 테스트할 때는 깊은 비교가 필요하다. 하지만 그 전에 "내가 정말로 확인하고 싶은 것이 무엇인가?"를 먼저 생각해보는 것이 좋다.

 

React에서의 객체 비교와 렌더링

자바스크립트의 참조 타입 비교 특성은 React와 같은 프레임워크에서 특히 중요한 영향을 미친다. React의 렌더링 메커니즘을 이해하는 데 핵심이 되는 부분이다.

 

useState와 객체 상태의 문제점

React에서 useState를 사용해 객체 형태의 상태를 관리할 때, 객체 내부의 프로퍼티만 변경하면 컴포넌트가 재렌더링되지 않는 경우가 있다. 이는 앞서 설명한 참조 타입의 비교 방식 때문이다.

function UserProfile() {
  const [user, setUser] = useState({
    name: "Alice",
    age: 30,
    preferences: {
      theme: "dark",
      notifications: true
    }
  });

  const updateTheme = () => {
    // 잘못된 방식: 내부 객체 직접 수정
    user.preferences.theme = "light";
    setUser(user); // React는 참조가 같은 객체로 인식하여 재렌더링하지 않음!
  };

  return (
    <div>
      <p>Name: {user.name}</p>
      <p>Theme: {user.preferences.theme}</p>
      <button onClick={updateTheme}>Change Theme</button>
    </div>
  );
}

위 코드에서 updateTheme 함수는 user 객체의 내부 프로퍼티를 직접 수정한 후 같은 객체 참조를 setUser에 전달한다. React는 동일한 참조(메모리 주소)를 가진 객체라고 판단하여 상태가 변경되지 않았다고 간주하고 재렌더링을 수행하지 않는다.

 

불변성을 유지하는 올바른 방법

React에서 상태 업데이트 시 불변성(immutability)을 유지하는 것이 중요하다. 상태 객체를 직접 수정하지 않고, 새로운 객체를 생성해야 한다:

function UserProfile() {
  const [user, setUser] = useState({
    name: "Alice",
    age: 30,
    preferences: {
      theme: "dark",
      notifications: true
    }
  });

  const updateTheme = () => {
    // 올바른 방식: 새 객체 생성 (스프레드 연산자 사용)
    setUser({
      ...user,
      preferences: {
        ...user.preferences,
        theme: "light"
      }
    });
  };

  return (
    <div>
      <p>Name: {user.name}</p>
      <p>Theme: {user.preferences.theme}</p>
      <button onClick={updateTheme}>Change Theme</button>
    </div>
  );
}

이 방식은 새로운 객체와 그 내부 객체들을 생성하므로, React는 참조가 변경되었음을 감지하고 적절히 재렌더링한다.

 

중첩 객체의 복사 문제

중첩이 깊은 객체의 경우, 스프레드 연산자만으로는 모든 레벨의 불변성을 유지하기 어렵다. 다음과 같은 해결책이 있다:

  1. 중첩 스프레드 연산자: 위 예제처럼 각 레벨마다 스프레드 연산자를 사용
  2. 구조 분해 객체 업데이트: 명확한 코드를 위해 변수를 사용
  3. const updateTheme = () => { const updatedPreferences = { ...user.preferences, theme: "light" }; setUser({ ...user, preferences: updatedPreferences }); };
  4. immer 라이브러리 사용: 복잡한 중첩 객체를 직관적으로 업데이트
  5. import produce from 'immer'; const updateTheme = () => { setUser(produce(user, draft => { draft.preferences.theme = "light"; // 불변성이 내부적으로 처리됨 })); };

 

React.memo와 객체 비교

React.memo로 컴포넌트를 최적화할 때도 객체 타입 props의 비교 방식을 이해해야 한다:

function ExpensiveComponent({ config, onAction }) {
  // 비용이 많이 드는 렌더링 작업...
  return <div>...</div>;
}

// 부모 컴포넌트
function ParentComponent() {
  const [count, setCount] = useState(0);
  
  // 렌더링마다 새 객체 생성 - memo가 제대로 작동하지 않음
  const config = { theme: "dark", size: "large" };
  
  // 렌더링마다 새 함수 생성 - memo가 제대로 작동하지 않음
  const handleAction = () => {
    console.log("Action triggered");
  };
  
  return (
    <>
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      <ExpensiveComponent config={config} onAction={handleAction} />
    </>
  );
}

// memo로 감싸도 부모가 렌더링될 때마다 새로운 config와 onAction이 생성되므로 항상 재렌더링됨
export default React.memo(ExpensiveComponent);

위 코드에서 React.memo는 얕은 비교를 수행하기 때문에, 매 렌더링마다 새롭게 생성되는 config 객체와 handleAction 함수가 이전과 다른 참조를 가지게 되어 최적화 효과가 사라진다.

 

해결책: useMemo와 useCallback

이 문제를 해결하기 위해 React는 useMemo와 useCallback 훅을 제공한다:

function ParentComponent() {
  const [count, setCount] = useState(0);
  
  // 의존성 배열이 변경될 때만 새 객체 생성
  const config = useMemo(() => ({ theme: "dark", size: "large" }), []);
  
  // 의존성 배열이 변경될 때만 새 함수 생성
  const handleAction = useCallback(() => {
    console.log("Action triggered");
  }, []);
  
  return (
    <>
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      <ExpensiveComponent config={config} onAction={handleAction} />
    </>
  );
}

이제 config와 handleAction은 컴포넌트가 재렌더링되어도 동일한 참조를 유지하므로, React.memo가 제대로 작동하여 불필요한 렌더링을 방지한다.

 

결론: 참조 타입 이해의 중요성

자바스크립트의 참조 타입 비교 특성을 이해하는 것은 React 애플리케이션의 성능 최적화에 필수적이다. 요약하자면:

  1. 상태 업데이트 시 불변성을 유지해야 React가 변경을 감지할 수 있다.
  2. 객체를 직접 수정하지 말고, 스프레드 연산자나 immer 같은 도구를 사용하여 새 객체를 생성하자.
  3. 함수형 업데이트를 활용하여 이전 상태를 기반으로 안전하게 업데이트하자.
    // 이전 상태에 의존하는 안전한 업데이트setUser(prevUser => ({  ...prevUser,  preferences: {    ...prevUser.preferences,    theme: "light"  }}));
    
  4. 복잡한 중첩 객체를 다룰 때는 immer 같은 라이브러리 사용을 고려하자.
  5. useMemo와 useCallback을 활용하여 참조 일관성을 유지하고 불필요한 렌더링을 방지하자.

이러한 React의 렌더링 메커니즘과 최적화 기법은 결국 자바스크립트의 참조 타입 비교 원리에 기반하고 있다. 따라서 자바스크립트의 기본 개념을 제대로 이해하는 것이 React를 효율적으로 사용하는 첫걸음이라고 할 수 있다.

 

 들어가며

최근 프로젝트에서 특정 텍스트를 블러 처리해야 하는 요구사항이 있었습니다. 처음에는 단순히 모자이크 효과만 생각했는데, CSS만으로도 여러 가지 흥미로운 방법이 있다는 걸 알게 되었습니다. 이전에는 텍스트를 감싸는 div를 relative로 만들고 그 안에 absolute, z-index 등을 조정한 반투명한 레이어를 올리는 식으로 구현했었는데 다른 방법이 없나 찾아보다가 공부하게 되었습니다.

 text-shadow로 블러 효과 구현하기

가장 먼저 시도해본 방법은 text-shadow와 color: transparent를 조합하는 것이었습니다:

<style>
.blur-text {
  color: transparent;
  text-shadow: 0 0 5px rgba(0,0,0,0.5);
}
</syle>

...

<div class="blur-text">이 텍스트는 블러 처리됩니다.</div>

...

이 방식의 특징은:

  1. 텍스트 자체는 투명하게 처리하고
  2. 그림자 효과로 블러된 모양을 만듭니다
  3. text-shadow의 세 번째 값(blur-radius)을 조절해서 블러 강도를 조절할 수 있습니다

 

 text-shadow의 응용

다른 활용방법은 없나 찾아보다가 예제를 만들어보게 되었습니다.

/* 네온 사인 효과 */
.neon-text {
  color: #fff;
  text-shadow: 
    0 0 5px #fff,
    0 0 10px #fff,
    0 0 20px #0ff,
    0 0 40px #0ff;
}

/* 텍스트 외곽선 효과 */
.outline-text {
  color: white;
  text-shadow: 
    -1px -1px 0 #000,
    1px -1px 0 #000,
    -1px 1px 0 #000,
    1px 1px 0 #000;
}

text-shadow의 값들을 여러 번 지정하면 레이어처럼 쌓이는 효과를 만들 수 있습니다.

네온 사인 효과의 경우:
1. 가장 안쪽부터 바깥쪽으로 점점 커지는 흰색 그림자(0 0 5px #fff, 0 0 10px #fff)로 빛나는 듯한 느낌을 주고
2. 그 위에 하늘색 그림자(0 0 20px #0ff, 0 0 40px #0ff)를 더해 네온 특유의 번짐 효과를 만듭니다

외곽선 효과는 좀 더 재미있는 접근인데요:
1. 텍스트의 상하좌우 네 방향에 1px 크기의 검정 그림자를 배치해서
2. 마치 텍스트에 외곽선을 그린 것처럼 보이게 만듭니다

 

 

 filter: blur 활용하기

/* 기본 블러 */
.blur-element {
  filter: blur(5px);
}

/* 블러와 밝기 조절 */
.blur-and-dark {
  filter: blur(3px) brightness(80%);
}

/* 여러 필터 조합하기 */
.filter-combo {
  filter: blur(2px) brightness(150%) grayscale(50%);
}

filter 속성은 포토샵의 필터처럼 다양한 시각적 효과를 줄 수 있습니다:

기본 블러의 경우:
- blur(5px)는 요소를 마치 핀트가 안 맞는 사진처럼 흐릿하게 만듭니다
- 픽셀값이 클수록 더 흐릿해집니다 블러와 밝기를 조절한 경우:
- blur(3px)로 약간의 흐림 효과를 주고
- brightness(80%)로 전체적인 밝기를 20% 낮춰서
- 마치 서리유리를 통해 보는 것 같은 효과를 만듭니다

필터 조합의 경우:
- blur(2px)로 살짝 흐리게 하고
- brightness(150%)로 밝기를 50% 높이고
- grayscale(50%)로 컬러를 절반만 남겨서
- 마치 빛에 바랜 듯한 효과를 만들어냅니다 이렇게 여러 필터를 조합하면 단순한 블러 이상의 다양한 효과를 만들 수 있습니다.

 예제 통합

<!DOCTYPE html>
<html>
<head>
  <style>
    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
      max-width: 800px;
      margin: 40px auto;
      padding: 0 20px;
    }
    
    .demo-grid {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
      gap: 20px;
      margin: 20px 0;
    }
    
    .demo-item {
      background: #f8f9fa;
      padding: 20px;
      border-radius: 8px;
      text-align: center;
    }
    
    .demo-label {
      font-size: 14px;
      color: #666;
      margin-bottom: 10px;
    }
    
    .blur-text {
      color: transparent;
      text-shadow: 0 0 5px rgba(0,0,0,0.5);
      font-size: 20px;
    }
    
    .neon-text {
      color: #fff;
      text-shadow: 
        0 0 5px #fff,
        0 0 10px #fff,
        0 0 20px #0ff,
        0 0 40px #0ff;
      font-size: 20px;
      background: #333;
      padding: 10px;
    }
    
    .outline-text {
      color: white;
      text-shadow: 
        -1px -1px 0 #000,
        1px -1px 0 #000,
        -1px 1px 0 #000,
        1px 1px 0 #000;
      font-size: 20px;
      background: #f0f0f0;
    }
    
    .blur-element {
      filter: blur(5px);
      font-size: 20px;
    }
    
    .blur-and-dark {
      filter: blur(3px) brightness(80%);
      font-size: 20px;
    }
    
    .filter-combo {
      filter: blur(2px) brightness(150%) grayscale(50%);
      font-size: 20px;
    }
  </style>
</head>
<body>
  <h2>CSS 블러 효과 데모</h2>
  
  <div class="demo-grid">
    <div class="demo-item">
      <div class="demo-label">text-shadow 블러</div>
      <div class="blur-text">안녕하세요</div>
    </div>
    
    <div class="demo-item">
      <div class="demo-label">네온 효과</div>
      <div class="neon-text">네온사인</div>
    </div>
    
    <div class="demo-item">
      <div class="demo-label">외곽선 효과</div>
      <div class="outline-text">외곽선</div>
    </div>
    
    <div class="demo-item">
      <div class="demo-label">filter: blur()</div>
      <div class="blur-element">블러 필터</div>
    </div>
    
    <div class="demo-item">
      <div class="demo-label">blur + brightness</div>
      <div class="blur-and-dark">블러 + 어둡게</div>
    </div>
    
    <div class="demo-item">
      <div class="demo-label">여러 필터 조합</div>
      <div class="filter-combo">필터 조합</div>
    </div>
  </div>
</body>
</html>

 

 실제 구현 결과

생각보다 네온사인은 적절히 사용하면 꽤 괜찮은 상황이 있을 것 같고

개인적인 저의 취향에는 text-shadow 블러를 가볍게 사용하거나 filter blur를 쓸 때는 brightness를 같이 사용하고 싶네요

 

각 효과의 특징과 활용법

실제로 구현해보니 각각의 방식이 장단점이 있었습니다

  1. text-shadow 방식:
    • 텍스트만 블러 처리가 필요할 때 좋습니다
    • 투명도와 그림자 강도를 조절해 다양한 효과를 낼 수 있습니다
    • 네온사인이나 외곽선 같은 특수한 효과도 만들 수 있습니다
  2. filter: blur 방식:
    • 요소 전체에 블러를 주고 싶을 때 좋습니다
      • 이미지에 대한 블러라던지 페이지 자체에 대한 블러처리에선 더 간편하게 줄 수 있습니다.
    • brightness, grayscale 등 다른 필터와 조합이 가능합니다
    • 블러 정도를 픽셀 단위로 정확하게 조절할 수 있습니다

※ 본 포스팅은 개인 기록용으로 반응형 웹에서 이미지나 요소의 비율을 유지하는 방법에 대해 고민하다가 알게된 내용을 정리한 것입니다. 혹시나 저와 비슷한 고민을 하시는 분이 계실까봐 기록으로 남깁니다.

 aspect-ratio로 반응형 웹에서 요소의 비율 유지하기

 

 들어가며

최근 캐러셀(슬라이더) 컴포넌트를 만들면서 이미지의 비율을 어떻게 유지할지 고민이 많았습니다. 처음에는 단순히 width와 height를 픽셀로 고정하거나 퍼센트(%)로 지정했는데, 이게 생각보다 쉽지 않더라구요. 고정 픽셀을 사용하면 모바일에서 깨지고, 퍼센트만 사용하면 부모 요소의 크기에 따라 이미지가 찌그러지는 문제가 있었습니다. div를 여러 개 중첩해서 해결할 수도 있겠지만, 성능 이슈가 걱정되었죠. 그러다 발견한 게 바로 `aspect-ratio` CSS 속성입니다. 

 

 aspect-ratio란?

`aspect-ratio`는 요소의 가로세로 비율을 설정하는 CSS 속성입니다. 예를 들어 16:9 비율을 유지하고 싶다면 다음과 같이 작성할 수 있습니다: 

element { aspect-ratio: 16/9; }

 

실제 사용 예시 - 기존 제가 했던 캐러셀 구현 방식의 문제점

처음에는 이런 식으로 구현했습니다

.carousel-wrapper {
  width: 100%;
  position: relative;
}

.carousel-container {
  width: 100%;
  height: 0;
  padding-bottom: 56.25%; /* 16:9 비율을 위한 padding hack */
  position: relative;
}

.carousel-item {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}

.carousel-image-wrapper {
  width: 100%;
  height: 100%;
  position: relative;
}

.carousel-image {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
...

<div class="carousel-wrapper">
  <div class="carousel-container">
    <div class="carousel-item">
      <div class="carousel-image-wrapper">
        <img src="image1.jpg" class="carousel-image" alt="" />
      </div>
    </div>
    <!-- 추가 슬라이드 아이템들... -->
  </div>
</div>

...

여기에서 저는 처음에는 구현이 일단 됐기 때문에 다른 작업을 하다가

실제로 api 연결을 하고 사진이 많아졌을 때 컴포넌트가 버벅거리는 경험을 하게 되었습니다.

그래서 코드를 점검해보고 고민해보는 시간을 갖게 되었고 제가 내렸던 이 방식의 문제점은

  1. div가 너무 많이 중첩됩니다 (wrapper -> container -> item -> image-wrapper -> image)
  2. position: absolute를 사용해야 하는 등 복잡한 position 설정이 필요합니다
  3. padding hack(padding-bottom: 56.25%)을 사용해야 해서 코드가 직관적이지 않습니다
  4. DOM 요소가 많아져서 성능에 영향을 줄 수 있습니다

였고 어떤 식으로 최적화 및 html 요소를 줄일 수 있을지 찾아보았습니다.

 

 aspect-ratio를 활용한 개선된 구현

이걸 aspect-ratio를 사용하면 훨씬 단순화할 수 있었습니다:

.carousel {
  width: 100%;
  overflow: hidden;
}

.carousel-item {
  width: 100%;
  aspect-ratio: 16/9;
}

.carousel-image {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
<div class="carousel">
  <div class="carousel-item">
    <img src="image1.jpg" class="carousel-image" alt="" />
  </div>
  <!-- 추가 슬라이드 아이템들... -->
</div>

 

단순히 계산해도 원래는
캐러셀 랩퍼 > 컨테이너 > 아이템 > 아이템 랩퍼 순으로 4뎁스나 필요했었는데

개선된 코드에서는 랩퍼 없이 캐러셀, 캐러셀 아이템 만으로 엄청 간소화되었습니다.

더 좋은 방법이 있지 않을까 고민하지 않고 아는 지식만으로 구현하려 했었기 때문에

무지에서 발생한 성능저하였고, 지금 구현은 된다고 하더라도 내가 하는게 최선인지 의심해보던 초심을 몇개월동안 잃어버렸던 것 같은데 포스팅할 좋은 경험이 되었습니다.

+ Recent posts