learn learn learn

이미지 로딩 실패시 처리, 웹폰트 관련내용, webpack dotenv, object-fit속성, 옵저버 관찰 등록 후 끊기, 옵셔널체이닝, input spellcheck, ios기기에서 safari접속 에러 내용, 모바일과 데스크탑 css 분리, react app 에서 IE 지원하기

이미지 로딩 실패시 ??

개발하다보면 이미지에 데이터를 불러와서 바인딩 해줄때, 데이터가 잘못되었거나 변경되었을 경우에는 이미지가 로드되지 않거나, alt속성에 따라 엑박 이미지가 나오게 된다. 그렇게되면 UI를 해칠 우려가 있다.

img tag onerror

이미지 태그에는 onerror라는 속성이 있다. src에 해당하는 속성으로 이미지를 불러오고, 문제가 생기면 onerror에 이미지로 대체한다.

import React from 'react
import errImg from './'
import replaceImg from './'

const ImgReplace = () => {

return <img src={errImg} onError={replaceImg} />

}

위와 같이 적용해 주었을 경우에는 errImg를 불러오는데 실패하면 replaceImg로 대체하여 로드한다.

background Img

위의 경우에는 이미지 태그에서 적용이 가능하나 백그라운드 이미지로 적용할 경우 적용이 안된다. 그러나 백그라운드 이미지에서도 간단한 방법이 있다.

import React from 'react
import errImg from './'
import replaceImg from './'

const ImgReplace = () => {

return <div
style={{
backgroundImg: 'url(errImg), url(replaceImg)'
}}
/>

}

background-img 속성에 url(이미지)를 2개를 넣어준다 먼저 앞에 이미지를 불러오고 실패 했을때 뒤에있는 이미지를 불러온다. 이렇게 이미지 로드시 실패에 관한 상황에 대처할 수 있다.

웹폰트 관련내용

웹폰트는 글라프로 구성된 모음이며, 각 글라프는 문자나 기호를 설명하는 벡터 모양이다. 그 결과, 두 가지 단순한 변수가 특정 글꼴 파일의 크기를 결정하며 Open Sans라는 인기있는 웹폰트에는 라틴어 그리스어 및 키릴어 문자를 포함하는 897개의 글라프가 들어있다.

폰트

  • web safe font- 일반적으로 시스템에 설치된 폰트이며 다운로드 없이 사용자에게 의도대로 표현이 가능하다.(Arial, Helvetica)
  • web font - 설치되어 있지 않아서 브라우저에서 다운로드 해야하는 폰트

왜 웹폰트가 나온걸까?

  • 시스템에 폰트가 없다면 폰트를 보여줄 수 없다.
  • 디자이너의 의도대로 표현이 안된다.
  • 가독성
  • 한국어는 특히, web safe font로 한계가 있다.

웹폰트의 형식

오늘날 웹에서는 네 가지 글꼴 컨테이너 형식인 EOT,TTF,WOFF,WOFF2가 사용된다. 여러 옵션이 있지만, 이전 브라우저와 최신 브라우저 모두에서 작동하는 단일 범용 형식은 없다. EOT는 IE 전용이고, TTF는 부분적인 IE 지원 기능이 포함되어있고, WOFF는 지원 범위가 가장 넓지만 몇명 이전 브라우저에서는 사용할 수 없다. WOFF2.0지원은 많은 브라우저에서 현재 진행중이다.

  • EOT: IE8 이하일 경우
  • TTF: 구형 안드로이드버전(4.4)에서 필요.
  • WOFF: 대부분의 모던 브라우저에서 지원
  • WOFF2: WOFF보다 압축률이 30%정도 더 좋음

웹폰트의 문제점?

단순하게 다운로드 시간만큼 렌더링이 느려진다. 특히 한글 폰트는 상대적으로 용량이 크다. 이점이 여려 문제를 야기한다.

적용

  • font-family 속성으로 폰트를 적용한다.
  • font-family에 명시된 글꼴이 설치가 되어있지 않다면 기본 글꼴을 보여준다.
body {
font-family: 'Nanim Gothic', sans-serif";
}

@font-face

@font-face {
  font-family: 'Nanum Gothic';
  font-style: normal;
  font-weight: 400;
  src: url(/font/NanumGothic.eot), url(....woff2) format('woff2'), url(....woff)
      format('woff'), url(....ttf) format('truetype') ...;
}

확장자를 적용할 때 순서

  1. woff2를 가장 앞에 써준다. 브라우저는 선언된 순서대로 지원 가능한 파일 형식을 다운받기에 압축률이 가장 좋은 woff2를 먼저 선언한다.
  2. format()은 반드시 써준다. 쓰지 않으면 브라우저는 지원 가능한 파일 형식이 나올 때 까지 순서대로 받음

적용 시 local 속성 세팅

local 없이 선언하게 되면 폰트 존재 유무와 관계없이 무조건 다운받게된다. 따라서 불필요한 리소스를 요청하게 된다. local 문법을 선언해주면 시스템에설치 되어 있다면 리소스를 요청하지 않는다.

src: local('Nanum-Gothic'), url(/font/NanumGothic.eot),
  url(....woff2) format('woff2'), url(....woff) format('woff'),
  url(....ttf) format('truetype');

subset

@font-face를 적용할때 unicode-range속성을 사용해서 지원가능한 unicode범위를 정해놓고 해당 속성에 일치하는 글자를 렌더링할 때 다운받는다.

@font-face {
  ... unicode-range: U+x xxx-xxxx, u+x xxx-xxxx;
}

다국어를 지원하는 사이트의 경우 유용하다.

FOIT과 FOUT

  • FOIT: Flash of Invisible Text 웹폰트가 로드될 떄까지 텍스트를 렌더링 하지 않다가 로드가 된 이후에 텍스트를 보여주는 동작.
    폰트가 로딩되지 않으면 웹페이지의 블락을 가져옴, 모던브라우저는 기다리는 제한 시간이 있다.
    chrome,safari, firefox
  • FOUT: Flash of Unstyled Tecxt 웹폰트가 로드될떄까지 기본 폰트를 보여주고 이후 글꼴을 대체하는 방식. 흔히 말하는 깜빡임이며 폰트에따라 자간, 높이에 따라 레이아웃이 변경될 수 있다.

왜??

폰트를 받는 시점이 텍스트를 렌더링 하는 시점과 경합이 일어나기에 그렇다. 두 가지 문제를 최소화 하는게 중요 FOIT을 방지하고 FOUT를 최소화 하는 방법으로!

font-display

@font-face에서 font-display라는 속성이 있다. 값들은 다음과 같다.

  • auto: 브라우저의 기본동작에 맡기는 방식
  • block: 짧은 차단 기간과 무한 스왑 기간을 부여한다. 글꼴이 로드되지 않으면 브라우저가 처음에 보이지 않는 텍스트를 그리지만, 로드되는 즉시 글꼴로 스왑한다.
  • swap: 글꼴에 0초의 차단 기간과 무한 스왑 기간을 부여한다. 응답이 올 떄까지 기다리고 그전까지 기본 폰트를 보여줌
  • fallback: 100ms 내외의 시간 동안만 블락하고 기본폰트를 보여준다. 응답이 오면 해당 폰트로 스왑하나 3s만 기다림.
  • optional: 100ms 내외의 시간동안만 블락하고 기본폰트를 보여줌 그 후 대체하지 않는다.

자바스크립트를 활용한 예

async function fontLoad() {
  const font = new FontFace('font', 'url(font.woff)')
  await font.load()

  document.fonts.add(font)
  document.body.classList.add('fonts-loaded')
}

위의 예처럼 className을 핸들링 할 수 도 있겠고, 다른 방법을 사용할 수 도 있겠다. 폰트 로딩 체크가 가능하다는점.

preload

html 의 head 부분에 link 태그를 추가한다.

<link rel="preload" href="./fonts/..." as="font" ... />

preload를 이용해서 폰트를 받으면 리소스를 다른것보다 먼저 요청한다. 요소들이 그려지기 전에 로드를 하고 FOUT과 FOIT이 없어지는 대신, 그만큼 렌더링이 느려진다. 사용여부와 관계없이 무조간 리소스를 받는다.
내가 preload를 이번에 적용하려고 할때 잘 적용이 되지 않았다. 원인은 아직 찾지 못했고 더 학습해야할 부분이다.
대부분의 경우에 preload와 font-display를 활용해서 대처가 가능하고 좀더 세부적인 컨트롤이 필요할 때 JS를 사용해서 적용해주면 될 것 같다.

Noto Sans 관련 https://m.blog.naver.com/PostView.nhn?blogId=flyteen85&logNo=221330192231&proxyReferer=https:%2F%2Fwww.google.com%2F

Webpack dotenv

환경변수를 관리할때 프로젝트의 루트 디렉토리에 .env 파일에 환경변수들을 관리하고 사용을 하며 그 내용을 간략하게 한번 정리했다. 신규 프로젝트를 생성해서 process.env에 접근을 했더니 접근이 되지 않았다. 해당 내용을 알아보니 webpack 세팅을 별도로 해주어야 했다. dotenv-webpack을 먼저 받는다.

$ npm i dotenv-webpack

그 후 webpack.config.js에서 셋팅을 해준다.

const Dotenv = require('dotenv-webpack')
//.. 그 후 플러그인 추가
module.export = {
...
plugins: [
new Dotenv({
path: '/.env',
// ... 나머지 옵션들
})
]
...
}

위와같이 웹팩 적용 후 다시 process.env에 접근하면 환경변수에 접근이 가능한것을 확인할 수 있다.

object-fit 속성

object-fit 속성은 대체되는 요소의 내용(img, video, object, svg)등 이 지정된 너비와 높이에 맞게 장착되는 방식을 지정한다. 따라서 이미지나 고정된 크기의 썸네일을 출력하는 다양한 경우처럼 제각각의 크기를 가진 오브젝트등을 넘겨받아 비율을 유지한 채로 일정한 크기를 재 가공하는 경우 유용하다. background-size속성과 매우 유사함.

img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
  • fill: 대체되는 요소의 내용이 지정된 너비와 높이에 따라 크기가 확대 혹은 축소된다. 요소를 가득 채울 수 있는 크기로 변화되면서 종횡비는 유지되지 않는다.
  • contain: 내용이 종횡비를 유지하면서 요소에 정의된 너비와 높이 안에서 가능한 많이 확대시킨다.
  • cover: 내용이 종횡비를 유지하면서 정의된 너비와 높이를 가득 채울때까지 확대된다.
  • none: 요소의 크기와 상관없이 기본 알고리즘에 의해 조정된다. 이 알고리즘은 원본의 크기에 가운데 정렬된 형태를 띈다.
  • scale-down: 내용의 크기를 아무것도 지정되지 않거나 혹은 contain이 지정되어 있는것처럼 변경한다.

object-position

object-fit 속성은 기본적으로 요소의 가운데로 이미지를 이동시키는데, 이 위치를 원하는 값으로 변경하는것이 object-position속성.

  • 기본값은 50%, 50% 이다.
  • 숫자형 px, em, %등이 사용되며, 키워드 top,left right, bottom이 사용될 수 있다.
    위와같은 속성을 사용하려면 background속성을 이용해야만 적용이 가능한줄 알았는데, 백그라운드 속성을 사용하지 않아도 일반 이미지태그에서도 백그라운드 포지션과 사이즈 속성이 적용이 가능하더라. 모르는게 정말 많기에 잘 학습해야겠다.

옵저버 관찰 등록 후 관찰 끊기.

특정 페이지가 마운트되고 요소관찰을 시작하는데, 요소들이 특정 조건에 따라 변경이 된 후, 옵저버를 다시 등록 하는데 옵저버가 2번이 등록이 되서, 난감했다. 그래서 옵저버의 콜백또한 2번이 실행되는 문제가 생겼다. DOM요소 안의 내용물은 바뀌지만, DOM자체는 그대로였기에, 2번 등록되는듯 했다. 그래서 처음에 든 생각은 언마운트 할 때 연결을 끊는 unobserve를 사용해서 해당 요소들을 unobserve하려고 했다.

export const unObserver = refs => {
  let observer: any
  observer = new IntersectionObserver(checkIntersect, {
    threshold: 0.5,
  })

  if (refs.current) {
    refs.current.forEach(el => {
      if (el) {
        observer.unobserve(el)
      }
    })
  }
}

unObserver 함수를 정의해서 export 해준 뒤

...
useEffect(() => {
//관찰 등록
  registerObserver(
  ...
  );

// 뒷정리에서 관찰 끊기
  return () => {
    unObserver(refs.current);
  };
}, [list]);
...

뒷정리에서 해당 unobserver함수를 실행시켰다. 함수는 정상적으로 작동했으나 관찰이 끊기지 않았다. 해당 원인은 intersectionObserver 생성자에 있었다. 기존 생성자는 함수에서 생성을 했었는데,

export const registerObserver = (refs) => {
let observer: any;
observer = new IntersectionObserver(checkIntersect, {
  threshold: 0.5,
});

  if (refs.current) {
      refs.current.forEach((el: any) => {
        if (el) {
          observer.observe(el);
        }
      });
    }

  ...
};

위와 같았다. 함수가 실행될때마다 새로운 observer인스턴스를 생성해서 각각의 별개의 observer인스턴스를 가진 것 이었다. 그래서 해당 파일에서 공통으로 사용되게끔 함수 밖으로 뺴주기로 하였다.

let observer: any
observer = new IntersectionObserver(checkIntersect, {
  threshold: 0.5,
})

export const registerObserver = refs => {
  if (refs.current) {
    refs.current.forEach(el => {
      if (el) {
        observer.observe(el)
      }
    })
  }
}
export const unObserver = refs => {
  if (refs.current) {
    refs.current.forEach(el => {
      if (el) {
        observer.unobserve(el)
      }
    })
  }
}

이제 정상적으로 언마운트할때 관찰을 끊고 새로 등록될떄 잘 등록된다.

옵셔널 체이닝

옵셔널체이닝은 새롭게 추가된 문법이다. null이나 undefined인 값이 반환되면, 즉시 중단하고 undefined를 반환한다. 보통 null이나 undefined의 값에 접근하여 함수를 실행시키면 오류가 발생하나, 오류 없이 undefined를 반환한다. 추가된지 얼마 안 된 문법이기에 구형 브라우저는 폴리필이 필요하다. 옵셔널 체이닝을 사용하면 프로퍼티가 없는 중첩 객체에도 에러 없이 안전하게 접근할 수 있다.

왜 옵셔널 체이닝이 필요할까?

예제를 보자. 여러 유저가 있는데 주소 정보를 가지고 있지 않은 유저가 몇몇이 있다. user.address.street에 접근해야 하는 상황이라면 에러가 발생할 수 있다.

let user = {} // 주소가 없다
alert(user.address.street) // error

요소가 페이지에 없는 경우에도 개발도중 에러가 발생한다.

const html = document.querySelector('.my-element').innerHTML // error

기존 방식에서 이런 문제들을 해결하기위해 && 연산자로 null체킹을 해주었다.

let user = {}

alert(user && user.address && user.address.street) // undefined 에러발생안함

옵셔널 체이닝 적용

?. 문법은 ?. 앞의 평가 대상이 Undefined나 null이면 평가를 머추고 undefined를 반환한다. && 연산으로 체크하던것을 옵셔널체이닝을 사용한다면 이와 같다.

let user = {}

alert(user?.address?.street) // undefined

user객체가 존재하지 않을떄

let user = null

alert(user?.address) // undefined
alert(user?.address.street) // undefined

user객체가 null 이기에 address.street접근 자체도 에러가 발생해야하지만, 평가 대상인 user에서 이미 undefined를 반환하여 에러가 발생하지 않았다. ?.은 문법이 위치해 있는 그자리에서만 동작하고 확장되지 않는다. 평가가 끝나고 그 즉시 평가를 멈추고 반환한다. 딱봐도 &&를 사용하는것보다 개발도 편하고, 가독성도 좋은것이 확인된다.

사용시 주의점

  • 옵셔널체이닝을 남용하면 안된다. ?.는 존재하지 않아도 괜찮은 대상에만 사용해야한다. 사용자 주소관련 user는 반드시 있어야 하는 값이지만. address는 필수값이 아니다. 실수로 user에 값을 할당하지 않았다면 바로 알아낼 수 있도록 해야한다.
  • ?.앞의 변수는 꼭 선언되어 있어야 한다. 변수 user가 선언되어있찌 않으면 user?.anything은 평가시 에러가난다.

단락 평가

?.은 왼쪽 평가대상에 값이 없으면 즉시 평가를 멈춘다. 따라서 ?. 오른쪽에 있는 함수 호출등의 부가 동작들은 평가가 멈추면 일어나지 않는다.

let user = null
let x = 0

user?.sayHi(x++) // 아무일 없다.

alert(x) // 0, 값이 증가안함

?.()와 ?.[]

?.은 연산자가 아니고 함수, 대괄호와 함꼐 동작하는 특별한 문법 구조체이다. ?.()은 존재 여부가 확실하지 않은 함수를 호출할 떄 사용 가능하다.

let user1 = {
  admin() {
    alert('관리자에요')
  },
}

let user2 = {}

user1.admin?.() // 관리자에요
user2.admin?.() // 실행 안함

위의 경우에는 user객체는 반드시 존재하기 떄문에 admin 프로퍼티엔 .만 사용해서 접근했다. 그리고 admin의 존재여부만 확인했다.
.대신 []를 사용해 객체 프로퍼티에 접근하는 경우엔 ?.[]사용이 가능하다. 프로퍼티 존재 여부가 확실하지 않은 경우에도 안전하게 프로퍼티를 읽을 수 있다.

let user1 = {
  firstName: '이름',
}

let user2 = null

let key = 'firstName'

alert(user1?.[key]) // 이름
alert(user2?.[key]) // undefined

?. 은 delete와도 함꼐 사용이 가능함.

delete user?.name // user가 존재하면 user.name 삭제

?.은 읽기나 삭제에는 사용이 가능하나 쓰기에는 불가능!!

user?.name = '이름' // syntaxError
// 해당 이유는 undefined = '이름' 이기때문에

input spellCheck 속성

개발을 하다가 모바일 기기에서 접속했을때, input폼에 텍스트를 입력하면 문법 검사를하고, 문법이 틀리면 아래 빨간 경고줄이 뜬다. 좋은 기능이지만, 불필요한 UI를 표현할때가 있어서 해당 기능을 끄고싶어서 방법을 찾아보았다.
input에 spellCheck라는 속성이 있는데, 해당 속성에 false값을 주면 입력필드에 맞춤법 검사를 비활성화 시킬 수 있다.

<input
    ...
    type="text"
    spellCheck={false}
/>

ios 기기 에서 safari 접속 관련 내용

인풋의 상단 보더

ios 기기로 사파리에서 접속했을떄, input을 보면 상단 보더는 약간의 셰도우가 들어가있고 보더도 뭔가 다른 느낌이다. 그래서 기존 레이아웃 의도와 달라질 수 있는데, 해당 부분을 처리하기위해서 쉐도우를 없애줘야 한다. 간단하게 css 를 적용하면 지울 수 있다.

.input {
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
}

위 내용을 추가해주면 기존 웹과 동일한 UI를 나타낼 수 있다.

인풋 클릭시 줌인? 되는 현상

아마 기본적으로 사용자 편의성을 위해 인풋창을 클릭 했을때, zoom-in이 되는 것 같다. 다만 줌인이되면 위의 보더 내용처럼 UI비율이 흐트러지게된다. 해당 현상을 방지하려면 처음에 css적용이 되어야할 것 같았는데, html 헤드에 메타태그를 하나 추가해주면 처리가 가능하다.

<head>
  <meta
    name="viewport"
    content="minimum-scale=1, initial-scale=1, width=device-width,maximum-scale=1, user-scalable=0"
  />
</head>

위의 내용 처럼 적용해주면 인풋을 클릭 하더라도 줌인이 되지 않는다.
ios 사파리환경에서 기본적으로 위와같은 내용들이 많이 있는것같아 개발하며 체크를 잘 해야겠다.

모바일과 데스크탑 css 분리

보통 반응형 레이아웃을 구성할 때 media query를 사용하여 브라우저의 넓이등 기준에 따라 다른 css를 적용하여 반응형 레이아웃을 구성한다. 하지만 html 요소 자체가 달라 질 수 있고, 오직 미디어쿼리만 사용하기에는 부족한 부분이 있을 수 있다.

react-device-detect

전에 한번 정리한 내용인데, 클라이언트측에서 모바일인지 아닌지 체크를 react-device-detect 라이브러리를 사용하여 체크해준다.

import { isMObileOnly } from 'react-device-detect'
...

isMobileOnly // 모바일이면 true 데스크탑이면 false 반환

불러오기

isMobileOnly값에 따라서 스타일 파일을 나눠서 적용해준다.

import { isMObileOnly } from 'react-device-detect'
import classnames from 'classnames/bind'

const styles = isMobileOnly ? require(Mobile.scss) : require(Desktop.scss)

const cx = classnames.bind(styles)

...

return <div className={cx('div')} />

react app 에서 IE 지원하기

React는 기본적으로 ie9, ie11 같은 구형 브라우저를 지원하지 않는다. 하지만, 방법은 있지만, 실행 속도등의 문제가 있을 수 있다.
react-app-polyfill 과 babel을 활용하면 문제를 해결할 수 있다.

react-app-polyfill

리액트 개발에서 사용하는 다양한 문법을 변환해주는 라이브러리이다. 필요한것만 포함하고 있어 작고 가벼운게 특징이다.
promise,window.fetch,Symbol,Object.assign,Array.from 과 같은 것들을 변환 해준다.
설치

npm i react-app-polyfill
yarn add react-app-polyfill

공식 npm을 보면 인스톨을 한 다음에 반드시 src/index.js의 첫번째 줄에서 import 해야한다고 나와있다.

src/index.js

import 'react-app-polyfill/ie11'
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import './styles/reset.scss'

ReactDOM.render(<App />, document.getElementById('root'))

ie9일 경우 동일하나 ie11에서 가져온것을 ie9로 바꿔준다.

import 'react-app-polyfill/ie9'
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import './styles/reset.scss'

ReactDOM.render(<App />, document.getElementById('root'))

stable

stable은 package.json의 browserslist 에 해당하는 브라우저에 대해 안정적인 코드를 사용할 수 있게 해준다. 적용이 잘 안될떄는 node_modules/.cache를 삭제하고 다시 실행한다.

import 'react-app-polyfill/ie11'
import 'react-app-polyfill/stable'
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import './styles/reset.scss'

ReactDOM.render(<App />, document.getElementById('root'))

@babel/polyfill

babel/polyfill은 react-app-polyfill 에 비해 사이즈가 크고, 필요없는 기능이 많다고 한다. 이전에는 async,await가 react-app-polyfill에서 지원이 안되서 babel/polyfill을 사용해야 적용이 되는것 같았는데, 지금 확인해보면 react-app-polyfill도 적용이 되는듯 하다. 다만 제너레이터 함수 같은것들을 위해 적용이 필요하다.
바벨 폴리필은 두 가지 패키지로 구성되어 있다.

npm install core-js regenerator-runtime

설치 후 index.js 상단에 import 한다.

import 'core-js/stable'
import 'regenerator-runtime/runtime'
import 'react-app-polyfill/ie9'
import 'react-app-polyfill/stable'

Written by@jaeyoung-son
배운것을 기록하는 공간입니다.

GitHub