객체지향 프로그래밍을

"잘" 해보기위해

처음부터 다시 공부중이다.

 

상속을 쓰다보면

늘 "객체지향"라는 개념이 혼동되었다.

 

내 기억속의 "객체지향"은

마치 자동차를 만드는 작업과 같았다.

 

예를들면,

스팅어가 탄생하는 과정을 한번 그려보자.

 

[상속의 개념을 적용한 예시]

a: "우리 스팅어라는 자동차를 만들거야,

아주 빠르고 스포티한 자동차지"

b: "그러면 엔진인 gv80에서 상속받고

타이어랑 엔진은 g70에서 상속받고...

뼈대는 탄탄하게 모하비에서 상속받도록 할까요?"

 

하지만, 실제로는 그렇지않다.

아주 작은 부품들이 모여서 하나의 덩어리 부품를 만들고.

그 덩어리 부품들이 모여서 하나의 자동차를 만드는 것이다.

 

이렇게 부품을 만들고

부품을 가지고 새로운 부품을 "구성" 하는것은

Composition(구성) 이라고 한다.

 

상속이 쓸모 없느것은 아니지만

먼저 어떻게 Composition을 할지 생각해보고

다음에 그러면 공통적인 무언가만 상속하는 형태로

가져가는게 좋지 않을까

하는 개발론적인 생각을 해보았다.

 

그럼, Composition(구성)의 개념을 적용하여

스팅어와 소나타를 만드는 과정을 코드로 살펴보자.

Github링크: https://github.com/jaekwangLee/oop_js_study

{
  interface Engine {
    run(): boolean;
    stop(): boolean;
    pressure: string; // 'weak' | 'strong' | 'normal';
  }

  interface Mission {
    mode: string; // auto | manual
    change(gear: number): number;
    clutch(): void;
    getGear(): number;
  }

  interface Frame {
    material: string; // Materials
    strength(): string;
    size(): { length: number; width: number; height: number };
    crash(): string;
  }

  interface Tire {
    brand: string;
    purpose: string;
    count: number;
    role(): void;
    pause(): void;
  }

  type Materials = "stainless" | "steel" | "aluminium";
  type TirePurpose = "winter" | "summer" | "for season";

  class GDI implements Engine {
    constructor(private hp: number, private torque: number) {}

    pressure = "strong";

    private heating = () => {
      console.log("heating ...♨");
    };

    private cooling = () => {
      console.log("cooling ...❄️");
    };

    run = () => {
      this.heating();
      console.log("engine is working with ", this.hp, this.torque);
      console.log("use power ", this.pressure);
      return true;
    };

    stop = () => {
      this.cooling();
      console.log("engine is stopping");
      console.log("power to zero ");
      return false;
    };
  }

  class MPI implements Engine {
    constructor(private hp: number, private torque: number) {}

    pressure = "weak";

    private heating = () => {
      console.log("heating ...♨");
    };

    private cooling = () => {
      console.log("cooling ...❄️");
    };

    run = () => {
      this.heating();
      console.log("engine working with ", this.hp, this.torque);
      console.log("use power ", this.pressure);
      return true;
    };

    stop = () => {
      this.cooling();
      console.log("engine is stopping");
      console.log("power to zero ");
      return false;
    };
  }

  class CVT implements Mission {
    mode = "";
    constructor(private gear: number, mode: "manual" | "auto") {
      this.mode = mode;
    }

    clutch = () => {
      console.log("press clutch!! ䷮");
    };

    change = (nextGear: number) => {
      if (nextGear > this.gear) return this.gear;
      if (nextGear <= 0) return this.gear;

      this.clutch();

      this.gear = nextGear;
      return this.gear;
    };

    getGear = () => {
      return this.gear;
    };
  }

  class BodyOnFrame implements Frame {
    constructor(
      private length: number,
      private width: number,
      private height: number
    ) {}

    material = "steel";

    strength = () => {
      return "hard";
    };

    size = () => {
      return {
        length: this.length,
        width: this.width,
        height: this.height,
      };
    };

    crash = () => {
      const strength = this.strength();
      switch (strength) {
        case "weak":
          return "die";
        case "hard":
          return "safe";
        default:
          return "injury";
      }
    };
  }

  class UniBodyFrame implements Frame {
    constructor(
      private length: number,
      private width: number,
      private height: number
    ) {}

    material = "aluminum";

    strength = () => {
      return "normal";
    };

    size = () => {
      return {
        length: this.length,
        width: this.width,
        height: this.height,
      };
    };

    crash = () => {
      const strength = this.strength();
      switch (strength) {
        case "weak":
          return "die";
        case "hard":
          return "safe";
        default:
          return "injury";
      }
    };
  }

  // 작업중
  class FourSeasonTire implements Tire {
    brand = "";
    purpose = "";
    count = 0;
    characters = [];

    constructor(brand: string, purpose: string, count: number) {
      this.brand = brand;
      this.purpose = purpose;
      this.count = count;
    }

    role = () => {
      console.log("rolling ...🚗");
    };

    pause = () => {
      console.log("pause with break ...🛑");
    };
  }

  class SummerTire implements Tire {
    brand = "";
    purpose = "";
    count = 0;

    constructor(
      brand: string,
      purpose: string,
      count: number,
      private characters?: Array<string>
    ) {
      this.brand = brand;
      this.purpose = purpose;
      this.count = count;
    }

    role = () => {
      console.log("rolling ...🚗");
    };

    pause = () => {
      console.log("pause with break ...🛑");
    };
  }

  class Car {
    constructor(
      private tire: Tire,
      private frame: Frame,
      private engine: Engine,
      private mission: Mission,
      private name: string
    ) {}

    turnOn = () => {
    console.log('Hi, myname is ', this.name);
      this.engine.run();
    };

    turnOff = () => {
      this.engine.stop();
    };

    start = () => {
      if (this.mission.mode === "manual" && this.mission.getGear() <= 0) {
        console.log("please, change the gear");
      } else {
          console.log("car is starting ~~ 🚗");
        this.tire.role();
      }
    };

    stop = () => {
      if (this.mission.mode === "manual" && this.mission.getGear() > 0) {
        this.mission.clutch();
        this.mission.change(0);
      }

      console.log("car is stopping ~~ 🚗");
      this.tire.pause();
    };
  }

  // make car !
  // tires
  const normalTires = new FourSeasonTire("Nexen", "four season", 4);
  const summerTires = new SummerTire("Kumho", "summer", 4);
  // engine
  const gdi = new GDI(320, 52);
  const mpi = new MPI(180, 23);
  // mission
  const cvt4 = new CVT(6, "auto");
  const cvt16 = new CVT(12, "auto");
  // frame
  const unibody = new UniBodyFrame(4950, 1900, 1720);
  const bodyon = new BodyOnFrame(4920, 1870, 1700);

  const sonata = new Car(normalTires, bodyon, mpi, cvt4, "sonata");
  const stinger = new Car(summerTires, unibody, gdi, cvt16, "stinger");

  sonata.turnOn();
  sonata.start();
  console.log('---------------')
  stinger.turnOn();
  stinger.start();
}

조금 길어보이지만 Composition의 개념을 통해

엔진/미션/타이어/뼈대로 구성된 자동차를 만드는 과정이다.

 

코드에서 알수 있듯이

각 부품멸 "설계도"에 해당하는 interface를 만들고

"설계도"를 가지고 부품(class)를 만들었다.

 

위에서는 두가지정도씩 부품을 만들었지만,

설계도를 가지고 얼마든지 다양한 부품을 찍어낼수있다.

 

그리고 각 설계도면을 가지고 만든 부품을

Car 라는 최종 부품, 산출물이 받게된다.

 

Car는

각 부품이 interface로 정의되어

아주 유연하게 설계되었다.

마치 그 유명한 "붕어빵 기계"와 같다.

 

위 코드를 실행한 결과

Hi, myname is  sonata
heating ...♨
engine working with  180 23
use power  weak
rolling ...🚗
car is starting ~~ 🚗
---------------
Hi, myname is  stinger
heating ...♨
engine is working with  320 52
use power  strong
rolling ...🚗
car is starting ~~ 🚗

이렇게 귀여운

자동차 두대의 동작을 살펴볼 수 있다.

728x90
반응형

이전 글

[1. 객체지향 프로그램의 의미를 알아보자]

https://honeystorage.tistory.com/287

 

아래 내용 관련 Github :)

https://github.com/jaekwangLee/oop_js_study

 

우리는 이전 글에서

객체지향의 탄생에는

절차지향 프로그래밍의 한계가 있음을

알게되었다.

 

이제는 구체적으로

객체지향의 특징들을 알아보자.

 

흔히 4가지 특징이 있다고 한다.

1. 캡슐화 : 내부의 특성은 안으로 숨기고 사용자에게는 사용법만을 노출시킨다.

2. 상속 : 부모의 특성을 자식이 물려받음

3. 다형성 : 하나의 클래스나 메소드가 다양한 방식으로

4. 추상화 : 중요한 정보만을 표현하여 공통의 속성이나 이름을 묶어 이름 붙임

 

한글로만 봐도 어려운 단어들이지만

하나씩 이해해보자

실제로 써보면

절차지향 프로그래밍에서는 못느꼈던

새로운 희열을 느낄수있다.

 

다시, 이전의 예시코드를 살펴보자.

이 짧은 예시코드안에

객체지향 특성을 모두 볼 수 있다.

{
  interface NormalCar {
    start(): void;
    stop(): void;
    run(): void;
    spec: Specifications;
    ownerName: string;
  }

  interface SuperCar {
    start(): void;
    stop(): void;
    run(): void;
    boost(): void;
    spec: Specifications;
    ownerName: string;
  }

  type Specifications = {
    tires: number;
    fuel: number;
    owner: string;
  };

  type OnOff = "off" | "on";

  class Vehicle {
    private engine: OnOff = "off";
    constructor(tires: number, fuel: number) {
      console.log(tires, fuel);
    }

    start = (): void => {
      console.log("start");
      this.engineToggle("on");
    };

    stop = (): void => {
      console.log("stop");
      this.engineToggle("off");
    };

    run = (): void => {
      console.log("keep going...");
    };

    boost = (): void => {
      console.log("speed up!!!");
    };

    get status() {
      return "car status : " + this.engine;
    }

    private engineToggle = (nextStatus: OnOff): void => {
      this.engine = nextStatus;
    };
  }

  class Car extends Vehicle {
    constructor(private tires: number, private fuel: number, private owner: string) {
      super(tires, fuel);
    }
    
    get spec() {
      return {
        tires: this.tires,
        fuel: this.fuel,
        owner: this.owner,
      };
    }
    
    get ownerName() {
      return this.owner;
    }
    
    set ownerName(newName: string) {
      this.owner = newName;
    }
  }

  // car 인스턴스 생성
  const myCar = new Car(4, 50, "steve");
  myCar.start();
  console.log(myCar.status);
  
  myCar.stop();
  console.log(myCar.status);

  // car 인스턴스 생성할 때 interface를 이용해 추상화
  const myFamilyCar: NormalCar = new Car(4, 60, "steve_family");
  // myFamilyCar.boost() <-- error

  const myHobbyCar: SuperCar = new Car(4, 48, "steve_hobby");
  myHobbyCar.boost();
}

1. 캡슐화

캡슐화는 ownerName을 통해 알수있다.

사용자는 ownerName을 읽거나 쓸수 있지만

class내에서는 owner라는 이름으로 존재한다.

 

사용자는 직접 owner를 수정하거나

읽어들일 수 없다.

 

마찬가지로 engine의 상태 또한

직접 변경할수 없으며,

시동을 걸거나(start) 끔으로써(stop)

상태를 변경할 수 있다.

 

이렇게 변수를 은닉함으로써

불필요하게 class내 멤버변수에 접근하여

값을 변경하는 경우를 방지할 수 있다.

 

2.  상속

Vehicle -> Car

위와 같은 방식으로 상속이 진행되었다.

 

Vehicle이라는 큰 카테고리 내에서 

공통의 속성이나 이름을 묶어주었다.

(모든 탈것에는 공통적으로  tires, fuel, start, stop, run 등의 기능이 존재함)

 

Car에는 Vehicle이 갖는 특성 외에

항상 주인이 정해지게 된다.

 

따라서, vehicle을 상속함과 동시에

생성자함수(constructor)에서 owner를 지정하게 해주었다.

 

다음, 제품별로 고유의 특성들을 또 갖게될텐대

porche 같은 고성능 차에는 boost라는 기능이 추가된다거나

주유구 방향 등을 설정할수도 있겠다.

 

3. 추상화

추상화에는 두가지 방법이 있다.

접근제한자 (private, public, protected)를 통한 추상화와

interface를 통한 추상화가 있다.

 

추상화의 목적은 사용성의 향상에 있다.

예를들어, 추상화처리를 하지 않았다면

Car를 통해 생성한 인스턴스를 사용하고자 할때

start,stop,run,boost,engine,owner,fuel,tires ...

등등 불필요한 properties를 보게될것이다.

 

개발자가 제공하고자하는 항목만을

추상화를 통해 제공할 수 있다.

 

위에서도 private으로 정보 접근을 제한함과 동시에

불필요한 옵션을 줄였고,

interface를 통해 Normal Car에서는 boost라는

property를 제공하지 않게 설정하였다.

 

3. 다형성

부모의 특성을 재정의하는

"오버라이딩"을 통해 이루어지게되는데,

자식들이 부모의 특성을 그대로 물려받기도 하지만

종종 변화를 주는 경우가 생긴다.

 

만약, 절차지향 프로그래밍이었다면

부분적으로 다르게 동작하는 자동차를 만들려면

대부분의 코드를 중복해서 사용하고

해당 부분을 수정해서 새로운 함수를 만들어야 할것이다.

 

객체지향 프로그래밍에서는

오버라이딩을 통해 원하는 부분만 수정한

새로운 객체를 쉽게 생성해낼수있다.

 

이를 다형성이라고 한다.

728x90
반응형

좀 더 나은 개발자가 되기위해

나를 되돌아보고

무엇을 다시 혹은 더 공부해야 될지

고민하던중

 

내가 OOP를 제대로 못하고있다고

생각이 들었다.

 

아직도 OOP를 제대로 못한다니

부끄럽지만 숨기지않고

인정하고 공부했을때

나아갈수 있는것이다.

 


아래 내용 관련 Github :)

https://github.com/jaekwangLee/oop_js_study

 

 

그러면 가장 기본적인

객체지향 프로그래밍의 의미를 알아본다.

 

시중에서 접하는 책들에서는

자동차, 커피머신, 로봇 등을 예시로 든다.

 

그 전에

더 이전부터 존재했던 개념인

절차지향 프로그래밍의 의미와

단점에 대해서 알아야

왜 객체지향이 등장했는지,

객체지향을 적용해야만 하는 이유를

알수있다.

 

절차지향 프로그래밍이 뭘까?

말그대로 절차를 중요시하며

순차적으로 실행되는 프로그래밍 기법을 말한다.

 

예를들어, 자동차가 있다면

시동 (start) 을 구현한다고 가정해보자.

절차지향적인 프로그래밍에서는 어떻게 할까?

// car.js
function car() {
  const fuel = 50;
  const brand = 'porche';
  const owner = 'steve';
  const tires = 4;
  
  function stop() {
    1. 사용자가 시동을 끈다.
    2. 액션을 엔진에 전달한다.
    3. 엔진이 신호를 받는다.
    4. 엔진이 동작을 멈춘다..
  }

  function start() {
    1. 사용자가 시동을 건다
    2. 액션을 엔진에 전달한다.
    3. 엔진이 신호를 받는다.
    4. 엔진이 동작을 시작한다.
  }
}
...

 (역시 개발자는 코드 구조로 봐야 이해가 쉽다)

 

이렇게 개발을 하면 단점이 뭘까?

무슨 문제가 있기에 객체지향을 해야하는 걸까?

 

1. 유지보수

누군가 저렇게 개발해둔 코드를 유지보수 하게 됐다면

지옥을 맛볼것이다.

시동 부분의 일부를 수정하는데도

이해해야 하는 코드가 너무나도 방대한것이다.

 

자동차 관련 코드 전체를 읽어야지

수정이 가능한 상황이 발생하기 쉽상이다.

 

2. 재사용성

start라는 시동 액션이 여기서만 쓰일까?

1) 개발중인 자동차와 다 똑같은데,

시동걸때 "시동!" 이라고 음성을 들려주는 자동차를 만들려면

수없이 중복되는 코드들을 짜야될것이다.

 

2) 자동차가 아닌 오토바이의 시동을 개발하게 되었다면,

자동차의 시동부와 액션이 같을 확률이 높지만 우리는

수없이 중복되는 코드들을 짜야될것이다.

 


 

이런 문제들을 알고

객체지향에 대해 다시 알아보자.

 

흔히 말하는 객체지향의 장점은

아래의 코드로 확인할 수 있다.

{
  interface NormalCar {
    start(): void;
    stop(): void;
    run(): void;
    spec: Specifications;
    ownerName: string;
  }

  interface SuperCar {
    start(): void;
    stop(): void;
    run(): void;
    boost(): void;
    spec: Specifications;
    ownerName: string;
  }

  type Specifications = {
    tires: number;
    fuel: number;
    owner: string;
  };

  type OnOff = "off" | "on";

  class Vehicle {
    private engine: OnOff = "off";
    constructor(tires: number, fuel: number) {
      console.log(tires, fuel);
    }

    start = (): void => {
      console.log("start");
      this.engineToggle("on");
    };

    stop = (): void => {
      console.log("stop");
      this.engineToggle("off");
    };

    run = (): void => {
      console.log("keep going...");
    };

    boost = (): void => {
      console.log("speed up!!!");
    };

    get status() {
      return "car status : " + this.engine;
    }

    private engineToggle = (nextStatus: OnOff): void => {
      this.engine = nextStatus;
    };
  }

  class Car extends Vehicle {
    constructor(private tires: number, private fuel: number, private owner: string) {
      super(tires, fuel);
    }
    
    get spec() {
      return {
        tires: this.tires,
        fuel: this.fuel,
        owner: this.owner,
      };
    }
    
    get ownerName() {
      return this.owner;
    }
    
    set ownerName(newName: string) {
      this.owner = newName;
    }
  }

  // car 인스턴스 생성
  const myCar = new Car(4, 50, "steve");
  myCar.start();
  console.log(myCar.status);
  
  myCar.stop();
  console.log(myCar.status);

  // car 인스턴스 생성할 때 interface를 이용해 추상화
  const myFamilyCar: NormalCar = new Car(4, 60, "steve_family");
  // myFamilyCar.boost() <-- error

  const myHobbyCar: SuperCar = new Car(4, 48, "steve_hobby");
  myHobbyCar.boost();
}

 

다음 장에서

위 코드에 대한 분석과

객체지향의 장점에 대해

이어서 공부해보도록 하자.

728x90
반응형

서비스를 운영하다보면 아무리 이미지 압축을 잘 하더라도,

이미지 자체가 워낙 큰 파일이라 로드에 시간을 길게 소요하는 경우가 발생합니다.

 

이런 경우를 대비해 이미지 로드에 부가적인 처리를 좀 해주면

멋스럽고 좋은 사용자 경험을 제공할 수 있습니다.

 

간단한 js와 css를 함께 응용해야하는데,

예제 코드들로 살펴보도록 하겠습니다.

 

[ 주요 키워드: classList, Image ]

 

1. 기본 핵심코드

1.1 html

<div class='image-wrapper'>
  <img class='image-thumbnail' src='/no-image.png' alt='이미지' />
</div>

1.2 css

.image-wrapper {
  background-color: #dadada;
  
  &.visible {
    background-color: transparent;
    transition: all 0.25s;
  }
  
  &.visible > .image-thumbnail {
    transform: scale(1);
    opacity: 1;
  }
}

.image-thumbnail {
  transform: scale(0.5);
  opacity: 0;
  
  width: 100%;
  height: auto;
  object-fit: cover;
}

1.3 js

<script>
  const source = "https://t1.daumcdn.net/tistory_admin/static/top/pc/img_common_tistory_200910.png";
  const image = new Image();
  image.setAttribute('src', source);
  image.onload = function() {
    const wrapper_node = document.querySelector('image-wrapper');
    const node = document.querySelector('image-thumbnail');
    
    wrapper_node.classList.add('visible')
    node.src = source;
  }
</script>

 

 

자, 이렇게하면 이미지가 로드된 후에 고급진 이펙트와 함께 이미지가 나타나게됩니다.

가능하다면 no image상태일때 이미지를 설정해줘도 좋겠죠?

 

 

하지만, 이렇게 단일 이미지를 로드할때보다는

여러 이미지를 로드할때 이것은 더욱 빛을 바랍니다.

예를들어,  Youtube의 영상 목록을 불러올때나 와디즈에서 펀딩 목록을 불러올때

image가 로드되는동안 기본 이미지(혹은 배경색)을 보여주다가

이미지가 로드되면 약간의 효과와 함께 이미지를 띄워줍니다.

 

 

이번에는 React 예제로 살펴보겠습니다.

예시의 편의성을 위해 데이터는 어딘가 서버로부터 불러왔다고 가정하겠습니다.

그리고 실제 개발에서 쓸수있도록, 페이지 스크롤에 따라 이미지 로드와 효과 적용을 핸들링 해보도록 하겠습니다.

 

const ImageList = ({ images ) => {
  return (
    <>
    {
      images.map((img, index) => {
        const image = new Image();
        image.load = function() {
          // how...?
        }
        return (
          <div key={"image-item-" + index.toString()} className='image-wrapper>
            <img className='image-thumbnail' alt='썸네일' />
          </div>
        );
      })
    }
    </>
  )
}

 

이전과 비슷한데  React라는 점, 그리고 단일 이미지가 아닌 list라는 부분에서 차이가 발생했습니다.

여기서 어떻게 각기 알맞는 이미지 태그에 이미지를 넣어줄 수 있을까요?

 

여러가지 방법이 있을 수 있지만,

여기서는 React hooks의 useRef를 사용해보도록 하겠습니다.

(최근에, hooks를 좀 익혀보는 중이거든요)

 

[ 주요 키워드: classList, Image, useRef, useEffect ]

 

<script>
import React, { useRef, useEffect } from 'react';

const ImageList = ({ images ) => {
  return (
    <>
    {
      images.map((img, index) => {
        const image_card = useRef(null);
        
        useEffect(() => {
          scorller();
          window.addEventListener('scroll', scroller, false);
          
          return window.removeEventListener('scoll', scroller, false);
        });
        
        const scroller = () => {
          const { classList: classes, offsetTop, clientHeight } = image_card.current;
          
          // 이미 visible 처리된 element는 중복작업 방지 - 쓸데없는 메모리낭비x
          if (classes.contains('visible')) return null;
          
          // 화면 스크롤 높이에따라 element가 노출되고 있는지를 체크
          const visible_timing = window.innerHeight + windwo.scrollY >= offsetTop - (clientHeight / 2);
          
          // scroll 위치까지 고려해서 이미지 로드 처리
          if (visible_timing) {
            const image = new Image();
            image.setAttribute('src', img);
            image.onload = function() {
              const node = image_card.current.querySelector('.image-thumbnail');
              if (node) {
                classes.add('visible');
                node.src = img;
              }
            }
          }
        }
        
        return (
          <div 
            key={'image-item-' + index.toString()} 
            className='image-wrapper'
            ref={image_card}
          >
            <img className='image-thumbnail' alt='썸네일' />
          </div>
        );
      })
    }
    </>
  )
}
</script>

 

이 처럼 React로 이미지 목록을 구현하는 방법을 살펴보았습니다.

어떤가요?

코드가 어렵다거나 하지는 않죠?

생각보다 간단하게 괜찮은 효과를 낼수 있음을 알 수 있습니다.

 

좀 더 나아간다면 ImageCard를 모듈화 하면 더 좋겠죠?

 

 

728x90
반응형

자바스크립트를 쓰는 개발자라면 매일 같이 마주하는

Object타입

이놈, 유용하지만 조심해서 써야한다.

 

코드를 쓰다보면 객체를 '복사'하게 되는 경우가 많은데 예를들면 아래와같다

const wallet = {
  card: 3,
  money: 0
};

const friend1_wallet = wallet;
const friend2_wallet = wallet;

console.log(wallet) // { card: 3, money: 0 }
console.log(friend1_wallet)  // { card: 3, money: 0 }
console.log(friend2_wallet)  // { card: 3, money: 0 }

 

자, 너무 나도 당연한 결과이다 그렇다면 이번엔 아래의 결과를 예상해보자

wallet.card = 4;

console.log(wallet);
console.log(friend1_wallet);
console.log(friend2_wallet);

 

아래의 결과가 정확히 예상되는가?

console.log(wallet); // { card: 4, money: 0 }
console.log(friend1_wallet); // { card: 4, money: 0 }
console.log(friend2_wallet); // { card: 4, money: 0 }

 

왜, 이런일이 생길까?

c와 같은 언어를 배우고 사용해본 개발자라면 메모리 참조에 대해서 알것이다.

const friend1_wallet = wallet; // 복사가 아닌 참조

 

friend1_wallet은 wallet이 가리키는 메모리 영역을 똑같이 바라보게 했을 뿐이기 때문에,

wallet을 바꾸면 당연하게도 firend1_wallet의 값이 바뀌게 되는것이다.

 

그렇다면, 

"지갑을 똑같이 복사한 다음 복사한 지갑에만 카드를 두개 더 넣어두고싶어요~"

라고 했을때 어떻게 처리를 해야할까?

 

우리의 javacript는 이를 위해 Object.assign() 메소드를 제공한다.

const wallet = {
  card: 3,
  money: 0
};

const friend1_wallet = Object.assign({}, wallet);
const friend2_wallet = Object.assign({}, wallet);

console.log(wallet) // { card: 3, money: 0 }
console.log(friend1_wallet)  // { card: 3, money: 0 }
console.log(friend2_wallet)  // { card: 3, money: 0 }

이렇게 하면 우리는 빈 객체에 wallet을 복사하게 된다.

 

따라서, 아래와 같이 실했했을때 결과를 예상해보자.

wallet.card = 6;

console.log(wallet);
console.log(freind1_wallet)
console.log(freind2_wallet)

 

결과를 예상해봤는가?

console.log(wallet); // { card: 6, money: 0 }
console.log(freind1_wallet); // { card: 3, money: 0 }
console.log(freind2_wallet); // { card: 3, money: 0 }

 

자, 이제 목적을 달성했다.

그런데...

"저는 지갑에 어머니가 맡기신 돈도 보관해야되요. 돈을 두가지로 분류해서 저장해주세요!" 라고 한다면

const wallet = {
  card: 3,
  money: {
    mother: 5000,
    mine: 100,
  },
};

const freind1_wallet = Object.assign({}, wallet);
const freind2_wallet = Object.assign({}, wallet);

console.log(wallet); // { card: 3, { mother: 5000, mine: 100 } }
console.log(freind1_wallet); // { card: 3, { mother: 5000, mine: 100 } }
console.log(freind2_wallet); // { card: 3, { mother: 5000, mine: 100 } }

 

자... 이런 상황에서 엄마가 1000원을 더 맡기셔서 wallet에 돈을 추가하는 상황이 되었다.

아래 상황의 결과를 예측해보자

wallet.money.mother += 3000;

console.log(wallet);
console.log(friend1_wallet);
console.log(friend2_wallet);

 

결과는 아래와 같다.

console.log(wallet); // { card: 3, money: { mother: 8000, mine: 100 } }
console.log(friend1_wallet); // { card: 3, money: { mother: 8000, mine: 100 } }
console.log(friend2_wallet); // { card: 3, money: { mother: 8000, mine: 100 } }

 

어라! 복사하면 참조 문제는 발생하지 않는것 아니었나요?

자, 여기서 이제 "얕은 복사" 라는 개념이 나온다.

Object.assign()은 얕은 복사를 해주는 메소드로 1Depth 까지만 그 특성을 유지해준다.

 

전개연산자도 이와 마찬가지다.

// 전개연산자 얕은복사 예시
const wallet = {
  card: 3,
  money: 0
};

const freind1_wallet = { ...wallet };

console.log(wallet); // { card: 3, money: 0 }
console.log(freind1_wallet); // { card: 3, money: 0 }

wallet.card = 4;

console.log(wallet); // { card: 4, money: 0 }
console.log(freind1_wallet); // { card: 3, money: 0 }


// 전개연산자 깊은복사...
const wallet = {
  card: 3,
  money: {
    mother: 5000,
    mine: 100
  }
};

const freind1_wallet = { ...wallet };

console.log(wallet); // { card: 3, money: { mother: 5000, mine: 100 } }
console.log(freind1_wallet); // { card: 3, money: { mother: 5000, mine: 100 } }

wallet.money.mother += 3000;

console.log(wallet); // { card: 3, money: { mother: 8000, mine: 100 } }
console.log(freind1_wallet); // { card: 3, money: { mother: 8000, mine: 100 } }

 

이런 문제를 해결하려면 어떻게해야 할까?

늘 그렇듯 방법이 있다.

1. 재귀함수를 통한 복사

2. JSON 메소드 응용하기

3. lodash등 라이브러리르 이용하기

 

1. 재귀함수를 통한 복사는 빈 객체를 생성하고 for...in 함수로 객체의 key,value를 빈 객체에 집어넣는 방식이다.

프로젝트 내에 deepCopy함수를 하나 생성해두고 두고두고 쓰면 제법 유용하다.

function deepCopy(obj) {
  const newObj = {};

  for (let key in obj) {
    if (typeof obj[key] === 'object') {
      newObj[key] = deepCopy(obj[key]);
    } else {
      newObj[key] = obj[key];
    }
  }

  return newObj;
}

 

2. JSON 메서드를 응용하는 방법은 간편하지만 퍼포먼스 적으로 좋지않다고 한다. 또한, File 타입 복사간 문제가 발생하니 사용을 지양하는게 좋을것으로 생각된다.

const obj = {
  a: 1,
  b: {
    b_1: 2,
    b_2: 3,
  }
}

const copiedObj = JSON.parse(JSON.stringify(obj));

 

3.  lodash 등 라이브러리 사용하기는 쉽고 강력하므로 개인적으로 이 방식을 권장하는바이다.

import _ from 'lodash';

const obj = {
  a: 1,
  b: {
    b_1: 2,
    b_2: 3,
  }
}

const copiedObj = _.cloneDeep(obj);
728x90
반응형

개발을 하다보면 사용자가 페이지에서 이탈하는 시점에 이벤트를 발생시켜야 하는 경우가 있다.

예를들어, 페이지 이탈전 저장알림을 띄운다거나 페이지에 머무는 시간을 체크하거나 할때 말이다.

 

이때 필요한게 이벤트 리스너 중에 'beforeunload'라는 것이다.

MDN 문서에 보면 beforeunload에 대해 다음과 같이 설명되어있다.

beforeunload 이벤트는 문서와 그 리소스가 언로드 되기 직전에 window에서 발생합니다. 
이벤트 발생 시점엔 문서를 아직 볼 수 있으며 이벤트도 취소 가능합니다.

 

MDN도 예시로 아래와 같은것을 들고있다.

beforeunload 이벤트를 사용하면 사용자가 페이지를 떠날 때 정말로 떠날 것인지 묻는 확인 대화 상자를 표시할 수 있습니다. 
사용자가 확인을 누를 경우 브라우저는 새로운 페이지로 탐색하고, 취소할 경우 탐색을 취소하고 현재 페이지에 머무릅니다.

 

자, 이 리스너가 무엇인지  언제필요한건지 알았으니 어떻게 쓰는지도 알아보자

window.addEventListener('beforeunload', onBeforeClosed, false);

const onBeforeClosed = (e) => {
  // 표준에 따라 기본 동작 방지
  event.preventDefault();
  // Chrome에서는 returnValue 설정이 필요함
  event.returnValue = '';
}

 

MDN 예시를 약간 변형해보았다. 브라우저 호환성을 꼭 참고해서 사용하도록 하자.

여기서 중요한 포인트가 있다.

 

예상하거나 의도하지 않은 동작이 일어나지 않게 하기위해서는

이벤트 리스너를 다시 제거해주는 작업도 진행해야한다.

window.removeEventListener("beforeunload", onBeforeClosed, false);

 

 

728x90
반응형

사용자기반 앱/웹 개발을 하다보면 Input은 안쓸수가없다고해도 과언이 아니다.

 

이때, 모바일 입력 편의성 제공을 위해 Input타입을 number로 잡아줬더니

 

숫자롤러가 나타나 디자인을 해치는 일이 발생한 경험이 있을것이다.

 

간단한 css추가로 숫자롤러를 없애보자.

 

input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
    /* display: none; <- Crashes Chrome on hover */
    -webkit-appearance: none;
    margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
}

input[type=number] {
    -moz-appearance:textfield; /* Firefox */
}

 

 

728x90
반응형

여기다가

사용중인 정규식을 하나하나 저장하고 업데이트 하고자한다.

매번 새로 짤 필요도 없고...

 

1. 이메일

/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,5})+$/

 

2. 휴대폰

/^01(?:0|1|[6-9])[0-9]{4}[0-9]{4}$/

728x90
반응형

사용자 기반 서비스를 만들다보니 Textarea에서 라인별 글자수 제한 및 총 라인 제한을 해야할 일이 생겼다

아래와 같은 코드로 해당 문제를 처리하였다.

 

필요한 사람들을 위해 공유한다.

const textareaLimitWordAndRow = (text, limitWordPerline, limeLines) => {
  let lines = text.split('\n');
  const charlimit = limitWordPerline; // 글자수 제한

  for (let i = 0; i < lines.length; i++) {
    if (lines[i].length <= charlimit) continue;
    let j = 0;
    let space = charlimit;
    while (j++ <= charlimit) {
      if (lines[i].charAt(j) === ' ') space = j;
    }
    
    lines[i + 1] = lines[i].substring(space + 1) + (lines[i + 1] || '');
    lines[i] = lines[i].substring(0, space);
  }

  return lines.slice(0, limeLines).join('\n');
};

 

 

728x90
반응형

프로젝트를 진행하다보면

목적은 다양하지만 결국 엑셀 출력기능을 사용하게된다.

어드민, 거래처 요구사항 등...

(대시보드가 있지만, 회사내적으로 파일로 관리를 해야한다나..)

 

그래서 이래저래 유용한 엑셀 출력기능을 알아보려고한다.

사용법도 간단하며 매우 유용한 기능이니 필요할때 한번씩 열람해서 보도록하자.

 

먼저, 아래와 같이 라이브러리를 설치하자.

npm i xlsx

(참고로, 해당 라이브러리는 브라우저와 nodejs에서 모두 사용가능하다.)

 

자, 이제 데이터를 엑셀로 출력해보자.

import XLSX from 'xlsx';

const data = [
  {
    index: 1,
    name: 'apple',
    price: '1,200'
  },
  {
    index: 2,
    name: 'grape',
    price: '3,500'
  },
  {
    index: 3,
    name: 'strawberry',
    price: '5,500'
  }
];

const exportDataToXlsx = ({ data, filename }) => {
  const worksheet = XLSX.utils.json_to_sheet(data); // excel sheet하단의 worksheet에 해당
  const new_workbook = XLSX.utils.book_new(); // excel 파일에 해당
  
  XLSX.utils.book_append_sheet(new_workbook, worksheet, 'SheetJS'); // excelsheet를 excel파일에 넣음
  XLSX.writeFile(new_workbook, filename + '.xlsx');
}

 

이와 같이 매우 짧고 간단한 코드로 간단히 excel파일을 출력할 수 있다.

응용한다면, data의 타입이나 기간등에 따라 sheet를 나누어 출력하거 파일을 구분할수도있을것 같다.

 

그 또한 매우 간단하지만, 비개발자 입장에서는 매우 고급기술처럼 보일수있다.

잘 알아뒀다가 고급기술인척 해보도록하자.

728x90
반응형