728x90

 

개요

최적화 기법

  • 이미지 사이즈 최적화
    • 너무 큰 사이즈의 이미지는 네트워크 트래픽이 증가해 서비스 로딩이 오래걸림.
    • 너무 작은 사이즈의 이미지는 화질이 저하되어 서비스 이용이 불편해짐
  • 코드 분할
    • 첫페이지 진입 시 당장 사용하지 않는 코드는 코드분할을 통해 따로 로드
  • 텍스트 압축
    • HTML, CSS ,JS 등을 다운로드 전에 서버에서 미리 압축.
  • 병목 코드 최적화
    • 너무 느리게 다운로드 되거나 느리게 실행되는 특정 자바스크립트 코드를 찾아 최적화

최적화 툴

  • 크롬 개발자 도구
    • Network 패널
    • Performance 패널
    • Lighthouse 패널
  • webpack-bundle-analyzer

최적화

Lighthouse 이용한 페이지 검사 및 개선

모든 최적화 포인트를 외우고 있으면 좋겠지만 현실적으로 불가능하므로 Lighthouse의 도움을 받는다. 검사 지표를 보고 Lighthouse가 제공한 Opportunity와 Diagnotics를 적용한다

개선 이전 검사결과

web vitals

  • FCP(First): DOM 컨텐츠의 첫번째 부분을 렌더링 하는데 걸리는 시간
  • Speed Index(SI): 페이지의 점진적 완성 과정의 속도. 시간에 따른 시각적 완성도 곡선 아래의 면적을 계산
  • LCP(Largest): 페이지 내 가장 큰 이미지나 텍스트 요소가 렌더링되기까지 걸리는 시간.
  • TTI(Time To Interactive): 사용자가 페이지와 상호 작용이 가능한 시점까지 걸리는 시간
  • TBT(Total Blocking Time): 페이지가 사용자 입력에 응답하지 않도록 차단된 시간을 총합한 지표. FCP,TTI 사이의 시간동안 발생
  • CLS(Cumulative Layout Shift): 페이지 로드과정에서 발생한 예기치 못한 레이아웃 이동

이미지 사이즈 최적화

비효율적인 이미지 분석

Diagnotics 항목을 보면 다음과 같은 비효율적인 이미지가 로드되는 상황이 나타난다.

병목 코드 최적화

Perfomance 패널에서 메인스레드의 작업을 상세하게 살펴보고 느린 작업이 무엇인지 확인. (Start profiling and reload page) 버튼 클릭.

CPU 차트

어느 타이밍에 어떤작업이 진행되는지 알 수 있다.

  • 노란색: 자바스크립트 실행
  • 보라색: 렌더링/레이아웃
  • 초록색: 페인팅
  • 회색: 기타
  • 빨간색: 병목지점. 특정 작업이 메인 스레드를 오랫동안 잡고 있을때

Network 타임라인

서비스 로드 과정에서 네트워크 요청을 시간 순서에 따라 보여줌. 오래걸리는 작업은 클릭하여 선택후 하단 탭의 summary를 통해 문제 파악 가능.

(여기서 JS 파일의 크기는 1.0MB)

플레임 차트

어떤 작업이 오래 걸리는지 파악 가능.

하단 탭

  • Summary: 선택 영역에서 발생한 작업 시간의 총합과 각 작업이 차지하는 비중
  • Bottom up: 가장 최하위에 있는 작업부터 상위 작업까지 보여줌
  • Call Tree: 가장 상위에 있는 작업부터 하위 작업 순으로 보여줌
  • Event Log: 브라우저 이벤트(ex. Loading, Experience Scripting, Rendering, Paint)

분석 방법

병목이 걸리는 Task를 선택후 하단 탭의 Bottom up 섹션을 통해 어떤 작업의 Self time이 가장 큰지 확인. 이 경우 removeSpecialCharacter의 작업이 병목을 유발함.

function removeSpecialCharacter(str) {
  const removeCharacters = ['#', '_', '*', '~', '&', ';', '!', '[', ']', '`', '>', '\n', '=', '-']
  let _str = str
  let i = 0,
    j = 0

  for (i = 0; i < removeCharacters.length; i++) {
    j = 0
    while (j < _str.length) {
      if (_str[j] === removeCharacters[i]) {
        _str = _str.substring(0, j).concat(_str.substring(j + 1))
        continue
      }
      j++
    }
  }

  return _str
}
 

실제로 보면 replace 함수를 사용하면 될것을 for 루프를 돌고있다.

 

코드 분할

분할 기준

  1. Network 타임라인를 통해 유난히 크고 다운로드가 오래걸리는 자바스크립트 파일을 확인.
  2. webpack-bundle-analyzer를 통해 해당 자바스크립트 파일이 어떤 코드로 이루어져 있는지 확인.

 

3. refractor 패키지의 용량이 너무 크다. 직접 설치한 패키지가 아니라면 lock.json파일을 확인하여 어느 패키지가 의존성을 가지고 있는지 확인. -> react-syntax-highlighter

4. react-syntax-highlighter 패키지는 Code Block 컴포넌트에서만 사용되니 굳이 처음 진입시 로드할 필요는 없다. 따라서 lazy load

 5. 결과확인

 

Page별 분할

  • 기존 Route설정파일에서 일반 import (List->Detail 페이지로 이동)

  • Page별 lazy import (List->Detail 페이지로 이동

 

  • 차이점
    • List 페이지 진입 시 진입 속도가 빨라졌다(8ms->2ms)
    • Detail 페이지 진입 시 또다시 js 파일을 로드하게 된다.(분할을 했기 때문에)

텍스트 압축

서버에서 gzip으로 압축해보내주자!

 

 

 

 

2

개요

최적화 기법

  • CSS 애니메이션 최적화
  • 컴포넌트 지연 로딩
    • 이전에는 페이지를 분할했지만 이제는 단일 컴포넌트를 분할하여 컴포넌트가 쓰이는 순간에 로드
  • 컴포넌트 사전 로딩
    • 분할된 코드를 preload하여 필요한 시점보다 먼저 다운로드.
  • 이미지 사전로딩
    - 이미지를 필요한 시점보다 먼저 다운로드하고 필요할때 로드

최적화 툴

  • 크롬 개발자 도구
    • Network 패널
    • Performance 패널
  • webpack-bundle-analyzer

최적화

애니메이션 최적화

Jank 현상

웹페이지나 앱에서 발생하는 시각적 끊김 현상. 브라우저가 정상적으로 60FPS로 화면을 그리지 못하기때문에 발생. Jank 현상은 주로 reflow, repaint로 인해 발생함.

  • reflow: 요소의 크기나 위치가 변경되어 레이아웃을 다시 계산하는 과정. width, height, margin, padding, position 변경 시 발생
  • repaint: 요소의 시각적 스타일만 변경되어 다시 그리는 과정. color, background-color, visibility, outline 변경 시 발생.

Jank 현상을 피하는 방법
- transform, opacity, will-change 같은 속성을 사용. 해당 Element를 벌도의 레이어로 분리하고 작업을 GPU에 위임함으로써 레이아웃 단계와 페인트 단계를 건너뜀. (=하드웨어 가속)

하드웨어 가속
현재 width 로 애니메이션을 조절하다보니 리플로우 작업(layout 시작~paint끝)이 1frame(1/60초)동안 끝나질 않는다.

이를 transform으로 변환해보면 결과는 다음과 같다.

컴포넌트 지연 로딩

서비스 첫화면부터 필요하지 않는 컴포넌트의 경우 지연로딩을 통해 chunk를 분리해준다. 여기서는 import한 Modal 컴포넌트의 경우 코드 분할해준다.

※ 컴포넌트를 lazy load 할때에는 suspense를 감싸주자.

컴포넌트 사전 로딩

모달을 지연로딩시키면 모달을 열었을때 네트워크를 통해 모달 코드를 새로 로드해야했다. 그러니 당연하게도 다음과 같이 모달이 뜨기전까지 지연이 발생한다.(click event ~ evaluate script)

이러한 지연을 해결하기 위해 렌더링이 완료된 직후 여유가 있을때 Modal 컴포넌트를 가져온다.

function App() {
   const [showModal,setShowModal] = useState(false)
   const [ImageModal, setImageModal] = useState(null);


   useEffect(() => {
    import('./components/ImageModal').then(res=>{
        // res.default는 함수형이기 때문에 setState 함수가 state 업데이트 함수로 오해함
        setImageModal(()=>res.default)
    })
  }, []); 

    return (
        <div className="App">
            <Header />
            <InfoTable />
            <ButtonModal onClick={() => { setShowModal(true) }}>올림픽 사진 보기</ButtonModal>
            <SurveyChart />
            <Footer />
            {showModal ? <Suspense fallback={null}><ImageModal closeModal={() => { setShowModal(false) }} /></Suspense> : null}
        </div>
    )
}
 

이미지 사전 로딩

모달을 열때 이미지가 제때 뜨지 않는다. 이로인해 CLS 이슈가 발생한다.

Modal을 로드할때 Modal 내 필요한 이미지들을 모두 로드한다.

function App() {
   const [showModal,setShowModal] = useState(false)
   const [ImageModal, setImageModal] = useState(null);


   useEffect(() => {
    import('./components/ImageModal').then(res=>{
        // res.default는 함수형이기 때문에 setState 함수가 state 업데이트 함수로 오해함
        setImageModal(()=>res.default)
        images.forEach(image=>{
            const img = new Image()
            img.src=image.original
        })
    })
  }, []); 

    return (
        <div className="App">
            <Header />
            <InfoTable />
            <ButtonModal onClick={() => { setShowModal(true) }}>올림픽 사진 보기</ButtonModal>
            <SurveyChart />
            <Footer />
            {showModal ? <Suspense fallback={null}><ImageModal closeModal={() => { setShowModal(false) }} /></Suspense> : null}
        </div>
    )
}

 

3

개요

최적화 기법

  • 이미지 지연 로딩
    • 첫화면에 당장 필요하지 않은 이미지가 로드 되지않도록 지연
  • 이미지 사이즈 최적화
    • 이전에는 CDN에서 로드된 이미지의 크기를 수정했지만 이번에는 정적 이미지를 최적화
  • 폰트 최적화
    • 커스텀 폰트 최적화
  • 캐시 최적화
  • 불필요한 CSS 제거
    • 불필요한 CSS 코드를 제거하여 파일사이즈 줄이기

최적화 툴

  • 크롬 개발자 도구
    • Coverage 패널: 웹페이지를 렌더링하는 과정에서 어떤 코드가 실행되었는지, 얼마나 실행되었는지(비율) 확인.
  • Squoosh: 이미지 압축 도구
  • PurgeCSS: 사용하지 않는 CSS 제거

최적화

이미지 지연로딩

이미지 영역이 화면에 보이는 순간 또는 그 직전에 이미지를 로드.

  • Scroll Event 감지 방식: scroll이 이동할때마다 콜백함수가 실행되기 때문에 내부에 무거운 로직이 들어가면 브라우저 메인스레드에 부담이감.
  • IntersectionObserver 방식: 스크롤 할때마다 콜백함수가 실행되는 것이 아님.
function Card(props) {
	const img = useRef(null)

	useEffect(()=>{
		const intersectionObserver = new IntersectionObserver((e)=>{
			if(e[0].isIntersecting){
				console.log(e[0].target)
				const imgComponent= img.current.querySelector('img')
				imgComponent.src = imgComponent.dataset.src	
				intersectionObserver.unobserve(e[0].target)
			}

		},{
			root: document,
			rootMargin: '0px',
			threshold:1
		})

		intersectionObserver.observe(img.current)

		return ()=>intersectionObserver.disconnect()
	},[])

	return (
		<div ref={img} className="Card text-center">
			<img data-src={props.image} />
			<div className="p-5 font-semibold text-gray-700 text-xl md:text-lg lg:text-xl keep-all">
				{props.children}
			</div>
		</div>
	)
}
 

※ img 컴포넌트에 대한 display:none->display:block 의 변경으로 위 문제를 해결 할 수 없다. 무조건 src를 넣는 시점을 제어해야한다.

이미지 사이즈 최적화

Card 내 이미지의 크기가 커 로딩속도가 너무 느리다.

  • 사이즈: PNG > JPG > WebP
  • 화질: PNG = WebP > JPG
  • 호환성: PNG = JPG > WebP

JPG/PNG 포맷의 이미지를 WebP 포맷으로 변환하여 고화질, 저용량 이미지로 최적화.

WebP를 지원하지 못하는 브라우저의 경우 JPG/PNG로 로드하게 해줘야한다. 이를 대비하기 위해 <picture> 를 이용한다.(https://www.w3schools.com/tags/tryit.asp?filename=tryhtml5_picture)

# 뷰포트 크기에 따라 구분 
<picture>
  <source media="(min-width:650px)" srcset="img_pink_flowers.jpg">
  <source media="(min-width:450px)" srcset="img_white_flowers.jpg">
  <img src="img_orange_flowers.jpg" alt="Flowers" style="width:auto;">
</picture>

# 이미지 포맷에 따라 구분
<picture>
  <source type="image/avif" srcset="img_pink_flowers.avif">
  <source type="image/webp" srcset="img_pink_flowers.webp">
  <img src="img_pink_flowers.jpg" alt="Flowers" style="width:auto;">
</picture>
 

동영상 최적화

동영상 압축 웹 사이트에서 동영상 너비,높이,Bitrate, Audio 유무 등을 조정하여 메모리 최적화.

단, 화질이 저화 되므로 영상이 중요하지 않다면 이를 보완하기위해 CSS 상으로 blur등의 필터를 씌운다

폰트 최적화

페이지 로드 후 폰트가 적용되어 텍스트 스타일이 변하는 모습이 관측된다.

브라우저에 따라 FOUT(텍스트 출력 후 폰트스타일 적용)/FOIT(폰트스타일 준비 완료 후 텍스트 출력) 방식이 다르다

폰트 적용 시점 제어

@font-face를 통해 font를 등록할때 font-display에 폰트 표현방식 설정

@font-face{
  font-family: BMYEONGSUNG;
  src: url('./assets/fonts/BMYEONGSUNG.ttf');
  font-display: fallback;
}
 
  • auto: 브라우저 기본 동작
  • block: FOIT (timeout=3s)
  • swap: FOUT
  • fallback: FOIT (timeout=0.1s) // 3초 후에도 불러오지 못한경우 기본 폰트로 유지
  • optional: FOIT // 네트워크 상태에 따라 기본 폰트로 유지할지 결정

 block방식을 선택할 경우 3초후 갑자기 텍스트가 나타날 수 있으니 fade-in 애니메이션을 함께 넣어주면 좋다

폰트 사이즈 감소

  • 압축률
    • EOT < TTF/OTF < WOFF < WOFF2
    • Transfonter라는 서비스에서 확장자 변환.
    • 우선순위에 순서로 src 설정
    • @font-face{
        font-family: BMYEONGSUNG;
        src: url('./assets/fonts/BMYEONGSUNG.woff2') format('woff2'), url('./assets/fonts/BMYEONGSUNG.woff') format('woff'), url('./assets/fonts/BMYEONGSUNG.ttf') format('ttf'),
        font-display: fallback;
      }
       
    • 서브셋 폰트 사용
      • 특정 영역에서 특정 문자의 폰트 정보만 가지고 있으면 된다. 이를 서브셋 폰트라고 한다.
      • Transfonter 서비스에서 원하는 문자를 넣고 폰트를 생성하면 서브셋 폰트를 생성할 수 있다.
      @font-face{
       font-family: BMYEONGSUNG;
       src: url('./assets/fonts/subset-BMYEONGSUNG.woff2') format('woff2'), url('./assets/fonts/subset-BMYEONGSUNG.woff') format('woff'), url('./assets/fonts/subset-BMYEONGSUNG.ttf') format('ttf'),
       font-display: fallback;
      }
       
    • Data-URI
      • transfonter 서비스에서 Base64로 encoding에서 이 결과물을 src에 넣으면 폰트 로드시간을 획기적으로 줄일 수 있다.

캐시최적화

Lighthouse Diagnostics를 보면 다음과 같은 진단이 있다.

실제로 저중에 하나의 결과를 보면 Response Header에 Cache-Control헤더가 없다

  • 웹 캐시 종류
    • 메모리 캐시: RAM에 저장하는 방식
    • 디스크 캐시: 파일 형태로 디스크에 저장하는 방식

캐시 최적화

Lighthouse Diagnostics 섹션에서 Serve static assets with an efficient cache policy항목을 펼쳐 캐시가 적용되지 않은 파일을 확인. 해당 파일들은 실제로 응답 헤더에 cache-control 속성이 없다.

  • memory cache: RAM에 저장하는 방식. 브라우저를 끄지않고 페이지 이동, 새로고침 했을때 많이 나타남.
  • disk cache: 파일 형태로 디스크에 저장하는 방식. 브라우저를 껐다 켰을때 나타남.
    어떤 캐시를 사용할지는 브라우저가 특정 알고리즘에 의해 알아서 처리. (사용자 제어 불가)

Cache-Control

FE 서버에서 설정해줘야한다!

  • cache 전략
    • no-cache: 캐시 써도 되는데, 매번 서버에 확인. revalidation에서 304 응답받으면 cache 사용.
    • no-store: 캐시 사용 금지. 매번 새로 받아올것
    • public: CDN, 프록시 서버에서도 캐시 가능
    • private: 사용자 브라우저에만 캐시 허용

적절한 캐시 유효시간

  • HTML: no-cache. 항상 최신버전의 웹 서비스를 제공하기 위함.
  • JS,CSS, Font: public, max-age=31536000. 해시값을 가지고 있어 파일이 변경되면 자동으로 캐시가 풀림
  • default: no-store

불필요한 CSS 제거

Lighthouse를 통해 상황을 파악하고 Coverage 패널을 통해 얼마나 불필요한 코드가 들어있는지 상세하게 알 수 있음.

하지만 위의 경우 모두 tailwind css라이브러리에서 추가한 것이다.
이를 해결하기위해 purgeCSS를 사용한다. (하지만 요즘 tailwind는 이런 문제를 자체적으로 해결해준다. v4 기준) 텍스트를 추출하여 사용여부를 판단하는 원리로 작동한다.

npm install -D purgecss

npx purgecss --css ./build/static/css/*.css --output ./build/static/css/ --content ./build/index.html ./build/static/js/*.js

 

4

개요

최적화 기법

  • 이미지 지연 로딩
    • IntersectionObserver 대신 react-lazy-load-image-component 라이브러리 사용
  • 레이아웃 이동 피하기
  • 리덕스 렌더링 최적화

최적화 툴

  • 크롬 개발자 도구
    • Network 패널
    • Performance 패널
    • Lighthouse 패널
  • React Developer Tools(Profiler)

최적화

레이아웃 이동 피하기

요소의 사이즈를 미리 예측하여 해당 사이즈만큼 공간을 확보

리덕스 리렌더링 최적화

React Developer Tools를 설치후 Highlight updates when components render옵션 활성화. 불필요한 리렌더링 발생현황을 파악.

 

 

Font 로딩 최적화 학습 자료

개요

이 문서는 웹 폰트 로딩 최적화와 관련된 핵심 개념들을 정리한 학습 자료입니다. 최근 커밋에서 적용된 font preload 최적화를 바탕으로 작성되었습니다.

1. <link> 태그란?

<link> 태그는 HTML 문서와 외부 리소스 간의 관계를 정의하는 HTML 요소입니다.

주요 특징

  • 빈 요소(void element): 닫는 태그가 없음
  • 외부 리소스 연결: CSS, 폰트, 아이콘 등의 외부 파일을 문서에 연결
  • 메타데이터 제공: 브라우저에게 리소스의 성격과 처리 방법을 알려줌

기본 문법

<link rel="관계" href="파일경로" type="MIME타입" />
 

2. <link> 태그의 주요 속성과 속성값

rel 속성 (relationship)

문서와 연결된 리소스 간의 관계를 정의합니다.

속성값설명예시

stylesheet CSS 스타일시트 연결 <link rel="stylesheet" href="style.css">
preload 리소스를 미리 로드 (높은 우선순위) <link rel="preload" href="font.woff2" as="font">
preconnect 외부 도메인과 미리 연결  
dns-prefetch DNS 조회를 미리 수행 <link rel="dns-prefetch" href="//example.com">
icon 파비콘 설정 <link rel="icon" href="favicon.ico">
canonical 정규 URL 지정  

🤔 헷갈리기 쉬운 3가지: canonical, dns-prefetch, preconnect 상세 비교

1. rel="canonical" - SEO를 위한 정규 URL 지정

목적: 검색엔진에게 "이 페이지의 공식적인 URL은 이것이다"라고 알려주는 것

사용 상황:

  • 동일한 콘텐츠가 여러 URL로 접근 가능한 경우
  • URL 파라미터로 인한 중복 페이지 문제 해결
  • SEO 최적화
<!-- 예시 1: 파라미터가 있는 URL의 정규화 -->

<link rel="canonical" href="https://shop.com/product?id=123" />

<!-- 예시 2: 모바일/데스크톱 버전 통합 -->

<link rel="canonical" href="https://www.example.com/article" />

<!-- 예시 3: HTTPS 버전을 정규 URL로 지정 -->

<link rel="canonical" href="https://example.com/page" />
 

실제 효과:

❌ canonical 없을 때:
Google 검색 결과에 중복 페이지들이 모두 나타남
- https://shop.com/product?id=123
- https://shop.com/product?id=123&ref=google
- https://shop.com/product?id=123&utm_source=ad

✅ canonical 있을 때:
Google이 정규 URL만 검색 결과에 표시
- https://shop.com/product?id=123 (정규 URL만 노출)
 

2. rel="dns-prefetch" - DNS 조회 미리 실행

목적: 외부 도메인의 DNS 조회를 미리 수행하여 나중에 해당 도메인의 리소스를 빠르게 로드

동작 과정:

  1. 브라우저가 dns-prefetch를 발견
  2. 백그라운드에서 DNS 조회 실행 (IP 주소 확인)
  3. 나중에 해당 도메인의 리소스가 필요할 때 DNS 조회 시간 절약
<!-- 예시: 외부 CDN에서 폰트를 로드할 예정 -->
<link rel="dns-prefetch" href="//fonts.googleapis.com" />
<link rel="dns-prefetch" href="//cdn.jsdelivr.net" />

<!-- 나중에 실제 리소스 로드 시 DNS 조회 시간 절약됨 -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter" />
 

타이밍 비교:

❌ dns-prefetch 없을 때:
1.  발견
2. fonts.googleapis.com DNS 조회 시작 (20-100ms)
3. DNS 조회 완료 후 연결 시작
4. 리소스 다운로드

✅ dns-prefetch 있을 때:
1.  발견
2. 백그라운드에서 DNS 조회 미리 완료
3. 나중에  발견
4. DNS 조회 생략하고 즉시 연결 시작 → 더 빠름!
 

3. rel="preconnect" - 연결 과정 전체를 미리 실행

목적: DNS 조회 + TCP 연결 + TLS 핸드셰이크까지 모든 연결 과정을 미리 완료

동작 과정:

  1. DNS 조회 (IP 주소 확인)
  2. TCP 연결 설정
  3. TLS/SSL 핸드셰이크 (HTTPS의 경우)
  4. 연결 준비 완료 상태로 대기
<!-- 예시: Google Fonts에서 폰트를 확실히 로드할 예정 -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />

<!-- 나중에 실제 리소스 로드 시 연결 과정 전체가 생략됨 -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter" />
 

dns-prefetch vs preconnect 비교:

dns-prefetch (가벼움):
1. DNS 조회만 미리 실행 (20-100ms 절약)
2. 연결이 확실하지 않을 때 사용
3. 여러 도메인에 대해 사용해도 부담 적음

preconnect (강력함):
1. DNS + TCP + TLS 모두 미리 실행 (100-500ms 절약)
2. 연결이 확실할 때만 사용 (리소스 소모가 더 큼)
3. 중요한 2-3개 도메인에만 사용 권장
 

4. 실제 사용 시나리오별 선택 가이드

🎯 시나리오 1: Google Fonts 사용

<!-- ✅ 권장: 확실히 폰트를 로드할 예정이므로 preconnect -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter" />
 

🎯 시나리오 2: 조건부로 외부 리소스 로드 가능

<!-- ✅ 권장: 확실하지 않으므로 dns-prefetch -->
<link rel="dns-prefetch" href="//cdn.example.com" />
<link rel="dns-prefetch" href="//analytics.google.com" />

<script>
  // 특정 조건에서만 로드
  if (userConsent) {
    loadScript('https://analytics.google.com/analytics.js')
  }
</script>
 

🎯 시나리오 3: 여러 URL로 접근 가능한 상품 페이지

<!-- ✅ 권장: SEO를 위한 canonical -->
<!-- 현재 URL: /product?id=123&color=red&size=large -->
<link rel="canonical" href="/product?id=123" />
 

5. 성능 측정으로 보는 실제 효과

DNS 조회 시간 측정:

// 성능 측정 코드
const observer = new PerformanceObserver(list => {
  for (const entry of list.getEntries()) {
    if (entry.name.includes('fonts.googleapis.com')) {
      console.log(
        'DNS 조회 시간:',
        entry.domainLookupEnd - entry.domainLookupStart
      )
      console.log('연결 시간:', entry.connectEnd - entry.connectStart)
      console.log('전체 시간:', entry.responseEnd - entry.startTime)
    }
  }
})
observer.observe({ entryTypes: ['navigation', 'resource'] })
 

실제 측정 결과 예시:

dns-prefetch 없음: DNS 조회 50ms + 연결 100ms = 150ms
dns-prefetch 적용: DNS 조회 0ms + 연결 100ms = 100ms (33% 개선)
preconnect 적용: DNS 조회 0ms + 연결 0ms = 0ms (100% 개선)
 

href 속성

연결할 리소스의 URL 또는 경로를 지정합니다.

<!-- 절대 경로 -->
<link rel="stylesheet" href="https://cdn.example.com/style.css" />

<!-- 상대 경로 -->
<link rel="stylesheet" href="/css/style.css" />
<link rel="stylesheet" href="./style.css" />
 

type 속성

리소스의 MIME 타입을 명시합니다.

MIME 타입설명예시

text/css CSS 파일 <link rel="stylesheet" type="text/css" href="style.css">
font/woff2 WOFF2 폰트 <link rel="preload" type="font/woff2" href="font.woff2">
font/ttf TrueType 폰트 <link rel="preload" type="font/ttf" href="font.ttf">

as 속성 (preload와 함께 사용)

preload할 리소스의 타입을 지정합니다.

<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin />
<link rel="preload" href="image.jpg" as="image" />
<link rel="preload" href="script.js" as="script" />
 

crossorigin 속성

교차 출처 요청 시 CORS 설정을 지정합니다.

<!-- 자격 증명 없이 요청 -->
<link rel="preload" href="font.woff2" as="font" crossorigin="anonymous" />

<!-- 자격 증명과 함께 요청 -->
<link rel="preload" href="font.woff2" as="font" crossorigin="use-credentials" />
 

기타 유용한 속성들

media 속성

특정 미디어 조건에서만 리소스를 로드합니다.

<link rel="stylesheet" href="print.css" media="print" />
<link rel="stylesheet" href="mobile.css" media="(max-width: 768px)" />
 

sizes 속성

아이콘의 크기를 지정합니다.

<link rel="icon" href="icon-32.png" sizes="32x32" type="image/png" />
<link rel="icon" href="icon-192.png" sizes="192x192" type="image/png" />
 

3. @font-face와 <link>의 관계

@font-face 란?

CSS에서 웹 폰트를 정의하고 사용할 수 있게 해주는 CSS at-rule입니다.

@font-face {
  font-family: 'MyCustomFont';
  src:
    url('/fonts/MyCustomFont.woff2') format('woff2'),
    url('/fonts/MyCustomFont.woff') format('woff');
  font-weight: normal;
  font-style: normal;
  font-display: swap;
}
 

1. 문제 상황

/* @font-face만 사용할 경우 */
@font-face {
  font-family: 'MyFont';
  src: url('/fonts/MyFont.woff2');
}

.title {
  font-family: 'MyFont', sans-serif; /* 이 시점에서야 폰트 다운로드 시작 */
}
 

문제점: 폰트가 실제로 사용되는 순간에야 다운로드가 시작됨 → FOUT/FOIT 발생

2. 해결책: preload + @font-face

<!-- HTML: 페이지 로드 즉시 폰트 다운로드 시작 -->
<link
  rel="preload"
  href="/fonts/MyFont.woff2"
  as="font"
  type="font/woff2"
  crossorigin="anonymous" />
 
/* CSS: 폰트 정의 및 사용 */
@font-face {
  font-family: 'MyFont';
  src: url('/fonts/MyFont.woff2') format('woff2');
  font-display: swap; /* 로딩 중 fallback 폰트 표시 */
}

.title {
  font-family: 'MyFont', sans-serif; /* 이미 로드된 폰트 즉시 적용 */
}
 

3. 실제 프로젝트 적용 예시 (커밋 기준)

<!-- _document.tsx -->
<link
  rel="preload"
  href="/fonts/HyundaiSansHeadPro-Regular.ttf"
  as="font"
  type="font/ttf"
  crossorigin="anonymous" />
 
/* globals.css */
@font-face {
  font-family: 'HyundaiSansHeadPro';
  src: url('/fonts/HyundaiSansHeadPro-Regular.ttf') format('truetype');
  font-weight: normal;
  font-style: normal;
  font-display: swap;
}
 

로딩 타임라인 비교

❌ preload 없이

  1. HTML 파싱 완료
  2. CSS 파싱 완료
  3. 레이아웃 계산
  4. 폰트가 필요한 요소 발견
  5. 폰트 다운로드 시작 ← 늦음
  6. 폰트 적용

✅ preload 사용

  1. HTML 파싱 시작
  2. preload 발견 → 폰트 다운로드 시작 ← 빠름
  3. CSS 파싱 완료
  4. 레이아웃 계산
  5. 폰트가 필요한 요소 발견
  6. 이미 로드된 폰트 즉시 적용

4. @font-face 상세 가이드

기본 구조와 속성

@font-face {
  font-family: 'FontName'; /* 필수: 폰트 패밀리 이름 */
  src: url('font.woff2'); /* 필수: 폰트 파일 경로 */
  font-weight: normal; /* 선택: 폰트 굵기 */
  font-style: normal; /* 선택: 폰트 스타일 */
  font-display: swap; /* 선택: 로딩 동작 */
  unicode-range: U+0000-00FF; /* 선택: 유니코드 범위 */
}
 

주요 속성 상세 설명

font-family

  • 폰트의 이름을 정의
  • CSS에서 이 이름으로 폰트를 참조
@font-face {
  font-family: 'MyCustomFont';
}

/* 사용 */
.text {
  font-family: 'MyCustomFont', Arial, sans-serif;
}
 

src

폰트 파일의 위치와 형식을 지정합니다.

/* 단일 형식 */
src: url('/fonts/font.woff2') format('woff2');

/* 여러 형식 (fallback) */
src:
  url('/fonts/font.woff2') format('woff2'),
  url('/fonts/font.woff') format('woff'),
  url('/fonts/font.ttf') format('truetype');

/* 로컬 폰트 우선 확인 */
src:
  local('Arial Bold'),
  local('Arial-Bold'),
  url('/fonts/arial-bold.woff2') format('woff2');
 

font-weight

폰트의 굵기를 지정합니다.

/* 숫자 값 */
font-weight: 400; /* normal */
font-weight: 700; /* bold */

/* 키워드 */
font-weight: normal;
font-weight: bold;

/* 범위 지정 (가변 폰트) */
font-weight: 100 900;
 

font-style

폰트의 스타일을 지정합니다.

font-style: normal; /* 기본값 */
font-style: italic; /* 이탤릭 */
font-style: oblique; /* 기울어진 */
 

font-display

폰트 로딩 중 표시 방식을 제어합니다.

값설명사용 사례

auto 브라우저 기본 동작 기본값
block 폰트 로드까지 텍스트 숨김 (최대 3초) 아이콘 폰트
swap 즉시 fallback 폰트 표시, 로드 후 교체 일반 텍스트 (권장)
fallback 100ms 후 fallback 표시, 3초 내 로드 시 교체 성능 중시
optional 100ms 후 fallback 표시, 캐시된 경우만 사용 매우 빠른 로딩 필요
/* 권장: 즉시 텍스트 표시, 폰트 로드 후 교체 */
@font-face {
  font-family: 'MyFont';
  src: url('/fonts/MyFont.woff2') format('woff2');
  font-display: swap;
}
 

unicode-range

특정 문자 범위에만 폰트를 적용합니다.

/* 영문만 */
@font-face {
  font-family: 'EnglishFont';
  src: url('/fonts/english.woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153;
}

/* 한글만 */
@font-face {
  font-family: 'KoreanFont';
  src: url('/fonts/korean.woff2');
  unicode-range: U+AC00-D7AF;
}
 

고급 패턴

1. 동일 패밀리 내 다양한 weight/style

/* Regular */
@font-face {
  font-family: 'MyFont';
  src: url('/fonts/MyFont-Regular.woff2');
  font-weight: 400;
  font-style: normal;
}

/* Bold */
@font-face {
  font-family: 'MyFont';
  src: url('/fonts/MyFont-Bold.woff2');
  font-weight: 700;
  font-style: normal;
}

/* Italic */
@font-face {
  font-family: 'MyFont';
  src: url('/fonts/MyFont-Italic.woff2');
  font-weight: 400;
  font-style: italic;
}

/* 사용 */
.text {
  font-family: 'MyFont', sans-serif;
}
.text.bold {
  font-weight: 700; /* Bold 폰트 자동 선택 */
}
.text.italic {
  font-style: italic; /* Italic 폰트 자동 선택 */
}
 

2. 가변 폰트 (Variable Font)

@font-face {
  font-family: 'VariableFont';
  src: url('/fonts/variable-font.woff2') format('woff2-variations');
  font-weight: 100 900;
  font-stretch: 75% 125%;
}

/* 사용 */
.text {
  font-family: 'VariableFont';
  font-weight: 350; /* 정확한 굵기 */
  font-stretch: 110%; /* 폭 조절 */
}
 

5. <link> 태그의 위치: <head> vs <body>

<head>  <link> (표준이자 권장사항)

장점

  1. 빠른 로딩: HTML 파싱 초기에 발견되어 즉시 다운로드 시작
  2. 렌더링 블로킹 방지: CSS는 렌더링을 블로킹하지만, 빠른 발견으로 지연 최소화
  3. 표준 준수: HTML 명세에 따른 올바른 사용법
  4. SEO 친화적: 검색엔진이 메타데이터를 빠르게 인식
<!DOCTYPE html>
<html>
  <head>
    <!-- ✅ 권장: head 내 위치 -->
    <link rel="stylesheet" href="/css/critical.css" />
    <link rel="preload" href="/fonts/main.woff2" as="font" crossorigin />
    <link rel="dns-prefetch" href="//fonts.googleapis.com" />
  </head>
  <body>
    <h1>Content</h1>
  </body>
</html>
 

로딩 타임라인 (head 내 link)

0ms    HTML 파싱 시작
1ms    <link> 발견 → CSS/폰트 다운로드 시작
...    HTML 계속 파싱
100ms  <body> 파싱
150ms  CSS 다운로드 완료
200ms  렌더링 시작 (스타일 적용됨)
 

<body>  <link> (특수한 경우)

사용 가능한 케이스

  1. Progressive Enhancement: 점진적 개선
  2. 조건부 로딩: 특정 상황에서만 필요한 리소스
  3. 지연 로딩: 중요하지 않은 스타일의 지연 로딩
<body>
  <header>메인 콘텐츠</header>

  <!-- 📱 모바일에서만 필요한 스타일 -->
  <script>
    if (window.innerWidth < 768) {
      document.head.appendChild(
        Object.assign(document.createElement('link'), {
          rel: 'stylesheet',
          href: '/css/mobile-only.css',
        })
      )
    }
  </script>

  <!-- 🎨 중요하지 않은 장식적 스타일 -->
  <section class="decorative-section">
    <link rel="stylesheet" href="/css/decorations.css" />
    <!-- 이 섹션의 스타일만 로드 -->
  </section>
</body>
 

로딩 타임라인 (body 내 link)

0ms    HTML 파싱 시작
100ms  <body> 파싱
150ms  <link> 발견 → CSS 다운로드 시작 (늦음!)
200ms  첫 렌더링 (스타일 없음)
250ms  CSS 다운로드 완료
300ms  리렌더링 (스타일 적용) → FOUC 발생!
 

성능 비교 및 권장사항

✅ 권장: Critical CSS는 head에

<head>
  <!-- 필수 스타일 -->
  <link rel="stylesheet" href="/css/critical.css" />

  <!-- 폰트 preload -->
  <link rel="preload" href="/fonts/main.woff2" as="font" crossorigin />

  <!-- Above-the-fold 콘텐츠에 필요한 모든 리소스 -->
</head>
 

💡 고급 패턴: 하이브리드 접근

<head>
  <!-- Critical CSS 인라인 -->
  <style>
    /* 핵심 스타일만 인라인으로 포함 */
    body {
      font-family: system-ui;
    }
    .header {
      background: #000;
    }
  </style>

  <!-- 폰트 preload -->
  <link rel="preload" href="/fonts/main.woff2" as="font" crossorigin />
</head>
<body>
  <header class="header">즉시 스타일 적용됨</header>

  <!-- 나머지 CSS는 비동기 로드 -->
  <link
    rel="preload"
    href="/css/main.css"
    as="style"
    onload="this.onload=null;this.rel='stylesheet'" />
  <noscript><link rel="stylesheet" href="/css/main.css" /></noscript>
</body>
 

6. 최적화 Best Practices

1. 폰트 로딩 최적화 체크리스트

✅ 필수 사항

  • 중요한 폰트는 <link rel="preload"> 사용
  • @font-face에 font-display: swap 설정
  • crossorigin="anonymous" 속성 추가
  • WOFF2 형식 우선 사용

✅ 권장 사항

  • 폰트 서브셋팅으로 파일 크기 줄이기
  • unicode-range로 필요한 문자만 로드
  • 시스템 폰트를 fallback으로 설정
  • 폰트 로딩 전략 설정

2. 실제 구현 예시 (프로젝트 기준)

<!-- _document.tsx -->
<head>
  <!-- 1. 핵심 폰트 preload -->
  <link
    rel="preload"
    href="/fonts/HyundaiSansTextPro-Regular.ttf"
    as="font"
    type="font/ttf"
    crossorigin="anonymous" />
  <link
    rel="preload"
    href="/fonts/HyundaiSansTextPro-Bold.ttf"
    as="font"
    type="font/ttf"
    crossorigin="anonymous" />
</head>
 
/* globals.css */
/* 2. @font-face 정의 */
@font-face {
  font-family: 'HyundaiSansTextPro';
  src: url('/fonts/HyundaiSansTextPro-Regular.ttf') format('truetype');
  font-weight: normal;
  font-style: normal;
  font-display: swap; /* 3. FOUC 방지 */
}

@font-face {
  font-family: 'HyundaiSansTextPro';
  src: url('/fonts/HyundaiSansTextPro-Bold.ttf') format('truetype');
  font-weight: bold;
  font-style: normal;
  font-display: swap;
}

/* 4. 시스템 폰트 fallback과 함께 사용 */
body {
  font-family:
    'HyundaiSansTextPro',
    -apple-system,
    BlinkMacSystemFont,
    sans-serif;
}
 

3. 성능 측정 및 디버깅

Chrome DevTools에서 확인하기

  1. Network 탭: 폰트 로딩 타이밍 확인
  2. Performance 탭: 렌더링 블로킹 확인
  3. Lighthouse: 폰트 최적화 제안 확인
  4. Coverage 탭: 사용되지 않는 폰트 확인

중요 메트릭

  • FCP (First Contentful Paint): 첫 콘텐츠 표시 시간
  • LCP (Largest Contentful Paint): 가장 큰 콘텐츠 표시 시간
  • FOUT/FOIT: 폰트 교체/숨김 현상

7. 주의사항 및 트러블슈팅

흔한 실수들

❌ 잘못된 예시

<!-- 1. crossorigin 누락 -->
<link rel="preload" href="/fonts/font.woff2" as="font" />

<!-- 2. type 속성 누락 -->
<link rel="preload" href="/fonts/font.woff2" as="font" crossorigin />

<!-- 3. @font-face와 경로 불일치 -->
<link rel="preload" href="/fonts/font.woff2" as="font" crossorigin />
 
@font-face {
  font-family: 'MyFont';
  src: url('/fonts/font.ttf'); /* ❌ 다른 파일! */
}
 

✅ 올바른 예시

<!-- 1. 모든 필수 속성 포함 -->
<link
  rel="preload"
  href="/fonts/font.woff2"
  as="font"
  type="font/woff2"
  crossorigin="anonymous" />
 
@font-face {
  font-family: 'MyFont';
  src: url('/fonts/font.woff2') format('woff2'); /* ✅ 동일한 파일 */
  font-display: swap;
}
 

디버깅 팁

1. 폰트가 로드되지 않을 때

// 폰트 로딩 상태 확인
document.fonts.ready.then(() => {
  console.log('모든 폰트 로드 완료')
})

// 특정 폰트 로딩 확인
if (document.fonts.check('16px MyFont')) {
  console.log('MyFont 사용 가능')
} else {
  console.log('MyFont 아직 로드되지 않음')
}
 

2. 네트워크 오류 확인

  • CORS 에러: crossorigin 속성 확인
  • 404 에러: 파일 경로 확인
  • MIME 타입 에러: 서버 설정 확인

요약

Font 로딩 최적화의 핵심은 <link rel="preload">와 @font-face의 조합입니다:

  1. <link rel="preload">: 페이지 로드 즉시 폰트 다운로드 시작
  2. @font-face: 폰트 정의 및 fallback 설정
  3. font-display: swap: 로딩 중 텍스트 숨김 방지
  4. crossorigin="anonymous": CORS 문제 해결

이를 통해 FOUT/FOIT를 방지하고 사용자 경험을 크게 개선할 수 있습니다.

웹 폰트 로딩 문제: FOUT vs FOIT

FOUT (Flash of Unstyled Text)

"스타일이 적용되지 않은 텍스트의 깜빡임"

동작 과정:

  1. 페이지 로드 시 즉시 fallback 폰트로 텍스트 표시
  2. 웹 폰트 다운로드 완료 후 웹 폰트로 교체
  3. 폰트 교체 시 깜빡임/레이아웃 변화 발생

FOIT (Flash of Invisible Text)

"보이지 않는 텍스트의 깜빡임"

동작 과정:

  1. 페이지 로드 시 텍스트를 숨김 (투명 처리)
  2. 웹 폰트 다운로드 완료까지 빈 공간만 표시
  3. 폰트 로드 후 갑자기 텍스트 나타남
728x90

monorepo 환경에서 사용하기 좋다.

polyrepo로 구성할 경우 프로젝트간 공용 컴포넌트에 대한 코드가 중복으로 늘어난다.

UI, 기능이 같은 컴포넌트를 하나의 repository에서 구성하고 싶은 경우 Monorepo로 구성하는 것이 이득이다.

 

Private npm

일반 npm package는 public npm에서 제공하는 package를 install에서 사용하는 구조이다.

만약 package를 private하게 관리해야 한다면 외부로 노출되지 않도록 npm registry를 구성해야한다.

회사 사내에서 재사용할 수 있는 모듈을 패키지화해서 공유하기 위해 private npm을 사용한다.

 

private npm 서비스의 종류로는 Verdaccio, Gihub packages, npm pro 등이 있다.

우리 회사는 node package의 버전 관리 용이성을위해 사용한다.

app1은 private npm의 package@0.1.0을 보는데 app2는 privatge npm의 package@0.1.1을 볼수있도록 설정.

Verdaccio vs Github Packages

Github Packages

  • github action 연동 가능. webhook 지원. => publishing 할때 혹은 publishing 이후 workflow customize 가능하다.
  • microsoft가 지원하기 때문에 유지보수가 계속된다.
  •  

Verdaccio

https://www.devh.kr/2020/Host-Publish-and-Manage-Private-npm-Packages-with/

  1. verdaccio 설치
  2. verdaccio 실행
  3. verdaccio 사용자 등록
  4. package 업데이트 정보 있을 시 npm publish

Best Practice

  • public package와 private package 네이밍 구분
    • 다음의 경우 @my-company/ 와 local- prefix를 가지는 package는 priavate package 이다.
packages:
   '@my-company/*':
     access: $authenticated // import packages할때 authenticated 되어야한다. 
     publish: $authenticated
    'local-*':
     access: $all
     publish: $authenticated
   '@*/*':
     access: $all
     publish: $authenticated
   '**':
     access: $all
     publish: $authenticated
  • private repository -> public repository(uplink) -> cache 순서로 보기때문에 이를 이용한다.
    • 만약 public repository package가 마음에 안든다면 같은 이름으로 override한다.
    • private package의 proxy설정을 해제한다. npmjs.org에서 가져오지 않게하기 위함이다.

Verdaccio + Docker

verdaccio private repository 자체를 docker image로 생성하여 어느 서버에서도 띄울 수 있도록 할 수 있다.

docker pull verdaccio/verdaccio:4
docker run -it --rm --name verdaccio -p 4873:4873 verdaccio/verdaccio

Verdaccio + Environment Variables

verdaccio는 permission이나 port 수정을 위해 환경변수를 사용한다.

728x90

우리의 일과는 50% 이상이 일하는데 소비된다. 삶이 일로써 소비된다면 일을 사랑하는 것이 삶을 사랑하는 가장 중요한 열쇠다. 일에 도전하고 동기를 부여하고 보상을 얻는 것은 아침에 일어나고 싶게 만든다.

잘 하는 사람이 되는 방법은 잘 하는 사람들이 모입 집단에서 한명이 되는 것이다.

 

자신만의 계획을 세워야한다.

어떻게 할지 이 책에서 차근차근 알아보자.

1. 자신의 시장을 선택하라. 집중할 기술과 사업 분야를 신중하게 골라야한다.

2. 자신에게 투자하라. 지식과 기술은 자신이라는 상품의 기반이 된다.

3. 실행하라. 뛰어난 기술만 갖춘 직원만으로는 회사 성과가 나오지 않는다. 직원은 기술을 바탕으로 가치를 만들어야한다. 

4. 마케팅하라. 자신을 전혀 알리지 않고 어떻게 인정받을수 있을지 알아야한다.

 

자신의 시장을 선택하라

  • 우연히 돌아가는 프로그램을 만들어서는 안된다. 어떻게 돌아가는지 이해해야한다.
  • 기술만 알아서는 안된다. 내가 속한(또는 속하려고 하는) 사업에 대해서도 철저한 이해가 필요하다.(ex. 제조, 금융 지식)
  • 내가 제일 못하더라도 잘하는 사람들 사이에 있어라. 그곳에서 보통만 해도 뛰어난 개발자가 될 수 있다.
  • 비주류 기술을 좋아할 수 있다는 것은 당신이 찐 개발자라는 증거가 될 수 있다. 이것 자체가 채용시 장점이 되는 것이고... 비주류 기술을 공부하면 기존에 알고있는 기술에 대해 더 깊게 알 수 있는 기회가 될 수도 있다.(ex Rescript)
  • 단순 테스터나 단순 코더가 되지말자. 코딩, 테스팅, 아키텍트 다재다능한 사람이 되자. 그러면서 깊이 있게 공부하여 전문가가 되자.
  • 작은 프로젝트를 두 번 정도 해보자. 한번은 내가 잘 아는 기술로, 한번은 경쟁 기술로 하되 그 기술의 독특한 방식을 사용해보자.
  • 당신이 IT분야에서 왜 일하나? 우연히 있게된 것인가? 보수가 좋아서? 부모가 권해서? 내 분야에 열정을 쏟을 수 없다면 나쁜 결과를 가져올 수 있다.
    • 2주간 일어나 일을 시작할 때마다 신나는 정도를 1부터 10으로 점수를 매기자. 급상승하는 부분이 있었나? 경향이 있었나? 항상 낮거나 항상 높았나? 평점은 얼마인가? 그 다음 2주간은 매일 아침에 10점으로 만들 계획을 짜라.
  • 나 자신을 기술로 정의하지 말자. 내가 한 일과 하고 싶은 일로 정의하자. 기술은 성공에 이르는 한 방법일 뿐이다.

자신에게 투자하라

경력을 위한 투자 전략 & 기술, 실력, 자신에게 투자하는 방법

  • 하나의 기술에 대해 '어떻게' 와 '왜'라는 질문을 계속 던져라. 이는 스스로 찾아야한다.
  • 무엇인가 정말 배우고 싶다면 내 자신이 멘토가 되어보라. 사람은 가르치면서 배우게된다.
  • 근무 시간을 연습 시간으로 삼지말자. 시간을 별도로 투자해야한다.
    • 몸에 익히기: 개발 환경의 여러 도구를 손에 익혀라. 개발자들이 활용할 수 있는 도구(ex. 정규식, 스트림 라이브러리, 컬렉션이나 리스트를 처리할 수 있는 유틸리티) 전체 를 습득해라.
    • 악보 읽기: 오픈소스를 읽고 기여해보아라. 중요한 것은 내가 보고 있는 것을 빠르게 이해하는 것이다.
    • 즉흥 연주: 사고를 날카롭게하고 즉흥 코딩 솜씨를 향상시킬 멋진 방법으로 스스로 제한 조건을 두고 연습하라. 
  • 특정 문제에 대한 해결 방법을 찾는 것보다 기존 코드를 자신의 스타일과 능력을 점검하는 확대경으로 삼아보자. 코드를 읽으면서 전에 해본적 없는 것들과 결코 생각해 보지도 못했던 것을 발견할 것이다.

실행

  • 파킨슨의 법칙: 끝내는데 필요한 시간에 맞추어 작업이 늘어난다.
    1달 완료 예정인 프로젝트를 1주일 안에 끝내보려고 해보라. 할 수 있다.
  • 상사에게 보고할 수준의 일을 매일 목표로 삼자. 간단하게 일간/주간/월간 목표를 세우고 성과를 추적하자.
  • 나는 회사의 좋은 투자 대상인가? 급여 인상을 당연하게 생각하지 말아라.
  • 회사에서 결코 편해지지 말자. 언제나 겸손해야한다. 매일 일어나면서 현재 지위에서 떨어질 수 있음을 상기하자.
    • 성공하면 성공할 수록 치명적인 실수를 저지를 수 있는데, 자기 결정에 의심을 하지 않는다는 것이다.
    • 나를 대신할 개발자가 언제나 있다고 생각하자. 이는 결국 내가 떠날수도 있다는 얘기가된다.
      나만 이해할 수 있는 코드, 나만이 유일하게 할수 있는 일이라 생각하는 것은 리팩토링과 문서화를 진행한다. 이러한 일들은 목록에서 없어져야 한다.
  • 유지보수를 꾸준히 하자. 버그를 고치고 사소한 기능 요구를 구현하고 계속 동작하게 만들라.
    • 오래된 웹 서비스를 현대적인 웹 브라우저의 기능을 이용할 수 있게 바꾼다.
    • 서비스의 품질을 점점 향상 시킨다.
  • 8시간밖에 없어! 부지런히 일해야지!! 라는 생각으로 일을 한다. 추가시간 없이 일하려하자. 열중하기의 힘은 대단하다.
  • 실수(버그)가 발생했을때 처리하는 법을 배우자.
    • 문제를 알게 되자마자 숨기지말고 드러내자
    • 빠른 해결을 위해 책임을 떠넘기지 말고 책임을 져라.
    • 해결책을 제시하라.
    • 도움을 구하라.
  • 계획을 아주 잘 세웠어도 삶은 위기와 재난의 연속이다. 당황&부정적인 대응으로 얻는 이익은 없다. 당황은 나의 능력을 떨어뜨린다.
    • 당황하지 않는 방법은 문제가 터져도 제3자 관점에서 상황을 분석하는 것이다.
  • 오후에 시간을 내서 다음날 하고 싶은  일을 모두 목록을 만들어둔다.
    • 그 날 하지 못한 일은 다음날 옮기고 과정을 계속한다. 계획하고 착수하는 리듬이 생기기 시작한다.

마케팅은 높으신 분들만 하는게 아니다

  • 내가 한 일을 윗사람이 알게 해라. 표현하지 않으면 모른다
  • 의사소통, 글쓰기를 위한 의사소통 능력을 기르자.
    • 매일 어떤 개발을 했는지 기록하고 설계의 타당성을 증명하고 어려운 기술적 또는 전문적 결정을 자세히 조사하라.
      글쓰기 연습이다
  • 직장 사람들이 나에대해 물을 수 있는 최악의 질문은 "그 사람은 뭘 하나요?" 이다. 영향력을 남기자.
  • 나를 다른 개발자로부터 남달라지게 만들 수 있는 것은 오픈 소스 소프트웨어 발표, 책과 기사쓰기, 컨퍼런스 발표이다.

자신의 강점을 유지보수하라

  • 연구, 투자, 실행, 마케팅의 루프를 돌텐데 너무 한 부분에 시간을 많이 쓰면 시대에 뒤쳐질 수 있음을 명시하자.
  • 나 자신을 측정하기 쉬운 방법은 신뢰할 만한 제3자를 이용하는 것이다. 멘토나 가까운 동료를 이용하자.
  • 내 경력 관리를 폭포수 모델로 관리하지 마라.(경력 사전설계X) 목표는 크게 세우더라도 도중에 꾸준히 수정하라. 목표를 바꿔나가는 것이다.
  • 오늘의 나는 어제의 나보다만 더 잘해지게 성장하면 된다. 조급해하지 말자.
728x90

SEO의 기본 세팅은 다음과 같이 설정한다.

  1. 페이지마다 캐노니컬 태그를 설정
    <!DOCTYPE html>
    <html>
    <head>
      <link rel="canonical" href="https://example.com/page.php" />
    </head>
    <body>
    ...
    </body>
    </html>
  2. 태그와 메타 설정
    이미지 태그에 alt, head에 메타데이터, title은 페이지마다 달라야 하며 link에 alt도 붙여야한다.
    apple.com은 SEO를 위해 다음과 같이 meta tag를 설정해 놓았다
    - apple home(www.apple.com)

    - apple watch(www.apple.com/watch)
  3. 페이지 속도 개선
    PageSpeedInsights에서 웹페이지 속도 개선에 대해 리포팅을 받아볼 수 있다.
  4. 구조화
    HTML5의 tag들이 sementic 하게 맞춰 설계되어야한다. https://search.google.com/test/rich-results
  5. 사이트맵의 정기적인 관리
     사이트맵을 주기적으로 갱신한다.

 

 

 

Vue 프로젝트를 google SEO에 등록하기 위해서는 크게 2가지 방법이 있다.

  1. vue-meta 이용하여 페이지별 meta tag 작성
  2. prerender-spa-plugin 이용하여 SSR 페이지 만들기 

첫번째 방법이 보다 편하므로 페이지별 meta tag를 이용하여 Vue 프로젝트의 SEO를 구현토록하자.

 

  1. vue-meta 라이브러리를 프로젝트에 설치한다.
    현시점 vue3에 적용가능한 vue-meta 라이브러리는 3.0.0-alpha.10 까지 나와있다
    npm i --save vue-meta@alpha
  2. vue가 vue-meta 플러그인을 사용할 수 있도록 해당 플러그인 라이브러리를 등록한다
    //main.ts
    import { createApp } from 'vue';
    import { createMetaManager } from 'vue-meta';
    import App from './App.vue';
    
    app
      .use(createMetaManager())
      .mount('#app');
  3. default meta tag를 설정한다
    - index.html 설정
    og란: open graph의 약자로  어떤 HTML 문서의 메타정보를 쉽게 표기하기 위해서 메타정보에 해당하는 제목, 설명, 문서타입, 대표 URL등을  정의할 수 있게해주는 프로토콜.
    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width,initial-scale=1.0">
        <meta name="robots" content="ALL">
        <link rel="icon" href="<%= BASE_URL %>favicon.ico">
        <title>식물의언어 : 식집사를 위한 식물 정보 플랫폼</title>
    
        <!--og, meta-->
        <!--기본적으로 웹에 설정해줘야하는 og 메타태그 및 Naver 블로그, 카카오톡 미리보기 설정-->
        <meta property="og:type" content="website">
        <!-- <meta property="og:url" content="https://www.plantslang.com/"> -->
        <meta id="meta_og_title" property="og:title" content="식물의언어">
        <meta id="meta_og_image" property="og:image" content="opengraph.webp">
        <!-- <meta property="og:description" content="식집사를 위한 식물 정보 플랫폼, 식물의언어를 찾아오세요"> -->
        <meta property="og:site_name" content="식물의언어">
        <meta property="og:locale" content="ko_KR">
        <meta property="og:width" content="1200">
        <meta property="og:height" content="630">
    
    
        <!-- <meta name="description" content="식집사를 위한 식물 정보 플랫폼, 식물의언어를 찾아오세요"> -->
        <!-- <meta name="keywords" content="식물의언어, 식물의언어, 식물, plantslang"> -->
    
        <!--phone 설정-->
        <meta name="theme-color" content="#365650">
        <meta name="msapplication-navbutton-color" content="#365650">
        <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
        <meta name="application-name" content="식물의언어">
        <meta name="msapplication-tooltip" content="식물의언어">
        
    
      </head>
      <body>
        <noscript>
          <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
        </noscript>
        <div id="app"></div>
        <!-- built files will be auto injected -->
      </body>
    </html>

    만약 description의 주석처리를 해제한다면 뒤에 vue-meta로 설정한 description과 중복이 발생하게 된다.
    중복이 발생하면 두개의 description을 이어 붙인다. 즉, 모든 페이지 별로 description이 중복되는 현상이 발생된다.
    https://www.searchenginejournal.com/google-on-how-it-handles-extra-meta-descriptions-and-title-tags/368600/#close
  4. 페이지(route)별 meta tag를 설정한다.

'VueJS' 카테고리의 다른 글

AWS 에 Jenkins와 Nginx 이용하여 vue project 올리기  (0) 2022.01.02
array / object prototype  (0) 2021.06.27
728x90

 

17에 비해 코드상으로 수정해야할 부분이 많지는 않다는 것이 다행이다. React18은 React 17에서 어떤점이 바꼈는지 알아보자

    1.  html과 js를 연결해주는 main.js안에서 사용하는 함수가 바꼈다.
      import ReactDOM from 'react-dom'
      import App from './App'
      
      //17
      //ReactDOM.render(<App />, document.getElementById('root'));
      
      //18
      const root = ReactDOM.createRoot(document.getElementById('root'));
      root.render(<App />);
    2. Concurrent Rendering
      동시에 일어난 다수의 상태 업데이트 처리하는 기법으로 urgent한 update는 오래 걸리거나 급박하지않은 update보다 우선되어진다.
      만약 엄청나게 많은 component가 동시에 변해야하는 상황이라면 
      React 17에서는 동시에 state 변화가 일어난다고 해도 먼저 일어난 것 순서대로 순차적으로 처리되는 반면
      React 18에서는 동시에 state 변화가 일어나야할 시 우선도를 보고 urgent한 update가 우선되어지고 urgent하지 않거나 오래걸리는 update는 background에서 수행된다.

      우선도 구분은 다음 API를 사용하여 우선도가 높은 상태변화와 낮은 상태변화를 구분한다. 
      - useTransition( )
      - startTransition( )
      - useDeferredValue( )

      useTransition startTransition useDefferedValue
      React에게 lower priority의  state update를 알려준다 - React에게 갱신된 값이 준비될때까지 이전값을 UI상 display 해야한다는 것을 알려준다
      - 2개의 value 중 하나는 업데이트가 빠르고 나머지 하나는 업데이트가 느린경우 느린 업데이트의 value에 대해서는 update전 값을 보여준다.
      함수형 컴포넌트에서 사용한다 hook이 사용될 수 없는 환경에서 사용한다
      [isPending, startTransition] 반환
      //isPending은 현재 background에서 state가 변화하고 있음을 알려준다
       
      startTransition(()=>setUser(user)); const deferredVal=useDeferredValue(value);
      import { useTransition } from 'react';
          
      const [isPending, startTransition] = useTransition();
      // Urgent
      setInputValue(input);
      
      // Mark any state updates inside as transitions
      startTransition(() => {
        // Transition
        setSearchQuery(input);
      })
      {isPending && <Spinner />}
      import { useState, useDeferredValue } from "react";
          
      function App() {
        const [input, setInput] = useState("");
        const deferredValue = useDeferredValue(text, { timeoutMs: 3000 }); 
      
        return (
          <div>
            <input value={input} onChange={handleChange} />
            <MyList text={deferredValue} />
          </div>
        );
       }
    3. State Batching 개선
      State Batching: 다수의 state 업데이트가 함께 실행된다. 그러므로 컴포넌트가 한번만 업데이트된다.
      function increaseCouterHandler() {
        //Two state update calls batched together
        // +3 씩 이뤄진다
        setCounter((curCounter)=>curCounter+1);
        setCounter((curCounter)=>curCounter+2);
      }
      
      function increaseCouterAsyncHandler() {
        setTimeout(()=>{
          //Two state update calls not batched when using react 17
          //+3 씩 이뤄지는데 오직 asynchronous 환경에서만 이뤄졌다
          setCounter((curCounter)=>curCounter+1);
          setCounter((curCounter)=>curCounter+2);
        })
      }
    4. Suspense 개선
      Suspense: Code(or data) fetching과 관련된 UI updates를 도와주는 component. 주로 lazy loading할때 사용한다.
      React 18에서는 Server side rendering 환경에서도 Suspense를 사용할 수 있다.
728x90

기본적인 React Application 을 실행시키기 위해서는 다음과 같은 프로세스를 실행한다.

nodejs설치 -> npm install -> npm run build -> server 실행

이러한 일련의 과정을 Docker 는 이미지로 생성해 순서대로 처리할 수 있다.

그리고 이런 이미지 설정파일을 Dockerfile에서 정의한다.

Dockerfile의 생김새는 다음과 같다.

# parent image
FROM node:17-alpine as build-stage

# Run Command (ex. COPY, npm install, ...)이 작업되는 directory 선언
WORKDIR /app

# all source code copy , COPY <src> <dest>
COPY . .

# dependencies
RUN npm install

# container가 어떤 포트를 사용할지 선언한다. app.js에서 4000포트 사용중이므로 4000포트를 expose 한다.
EXPOSE 4000

# 다음 명령어가 안되는 이유: image는 container의 blueprint이지 application을 실행하는 곳이 아니다. container는 image의 running instance이다.
#RUN node app.js

# container가 RUN을 시작한 후 서버가 작동하게 하기 위해서 다음과 같이 CMD 명령어를 쓴다.
CMD ["node", "app.js"]

# 빌드하는 명령어 myapp: image 이름, . : dockerfile build하는 장소로부터의 상대 경로
#docker build -t myapp .

parent image 설정 -> work directory 설정 -> work directory에 source code 복사 -> package dependency 설치 -> 포트 설정 -> 서버 실행

의 순서대로 설정된다.

여기서 주의해야할 것이 2가지가 있다.

  1. WORKDIR
    • 작업이 이뤄지는 경로로서 WORKDIR이 선언된 이후 모든 작업은 선언된 directory에서만 이뤄진다.
  2. EXPOSE
    • 호스트와 연결할 포트 번호 설정.
    • db: 
        image: mysql:latest 
        expose: 
          - "3306" 
      
      node: 
        image: node:latest
      위의 경우 node는 mysql에 3306포트로 접근할 수 있다.
  3. RUN vs CMD
    • 두 명령어 모두 Command를 실행시켜주는 명령어이다.
      하지만 RUN은 image를 구성하는데 사용하는 명령어로 image는 container의 blueprint이지 application을 실행하는 역할이 아니다. 왜냐하면 container는 image의 running instance이기 때문이다.
      CMD는 모든 image구성이 끝난 후 실행할 Command를 실행시켜주기 위해 사용한다.
728x90

Docker Image

container의 설계. Application이 작동하기 위해서 필요한 요소들을 의미한다.

Image 구성

  • Parent Image : OS 또는 runtime environment 를 의미 (ex. node, python, jdk...)
    • docker hub에서 parent image를 찾을 수 있다.

 

Docker Container

여러 이미지들을 소유한 실행가능한 인스턴스. Application을 실행시켜준다.
즉, Image들이 실행되면 Container가 생성되면서 application이 동작한다.
하나의 독립적인 process라 보면 편하다

작동 원리 

  • 같은 구성의 이미지를 가진 Container는 모두 같은 방식으로 작동한다.
  • Server에 어떤 환경이 구성되어 있던지 Container는 Image의 환경을 따라간다. (Isolated)
728x90

Storybook 이란?

  • UI component를 위한 개발 환경과 Playground
  • 컴포넌트를 독립적으로 생성해준다.
  • 격리된 개발 환경에서 이러한 컴포넌트를 대화식으로 보여준다. => react 어플리케이션의 외부에서 돌아간다.
    • 개발된 컴포넌트들이 어떻게 다른 Props를 가지는지 보여준다.
    • 동적으로 Props나 Accessibility score를 바꿔준다.

Storybook 시작하기

  1. npx create-react-app react-story-v6
  2. npx sb init => Add Storybook
    - npm run storybook / npm build-storybook : development mode에서 storybook 실행/ storybook 빌드
    - main.js : storybook의 configuration file
    - preview.js : 내가 작성한 story의 configuration file
    - stories/~.stories.mdx: storybook의 landing page
    - stories/~.js : component
    - stories/~.stories : component와 연관이 있는 story를 모아놓은 파일. story를 export 한다.
  3. npm run storybook : component의 landing page들을 볼 수 있다.

+ Recent posts