최근 프로젝트 위주로 개발을 진행했고, 특정 기술만 주로 사용하다보니 시야가 좁아진다는 느낌을 많이 받았습니다. 그래서 이번 기회에 React Hooks부터 JS, HTML 태그들, CSS까지 전체적으로 복습해보며 시야를 넓히는 연습을 해볼려고 합니다. 이 과정을 공유하여 개발을 공부하는 분들에게도 도움이 되었으면 하는 마음에 이렇게 블로그에 남기게 되었습니다!

 

처음 다루어 볼 녀석은 React의 Hook들입니다. 이 글을 읽고 있는 여러분들도 알고 계시겠지만 Hook이 나오게 된 배경을 아주 간단하게 이야기 하고 바로 본론으로 넘어가겠습니다. 지금부터 편하게 리액트와 훅이라고 부르겠습니당 하핳

 

리액트는 처음에 컴포넌트를 상속 받는 클래스형으로 컴포넌트로 만들었고, state와 리액트의 라이프 사이클을 이용했습니다. 하지만 매번 컴포넌트를 상속 받아서 컴포넌트를 만들어야 하는 방식은 불편했고, 이에 대한 해결책으로 나온 것이 바로 함수형 컴포넌트입니다. 여기서 문제는 함수형 컴포넌트는 기존 클래스형에서 사용하던 state와 리액트의 라이프사이클을 이용하기에는 어려움이 있었다는 것입니다. 이러한 문제를 해결해 준 주인공이 바로 Hook들입니다. 대표적으로 state를 함수형 컴포넌트에서 사용할 수 있는 useState와 리액트 라이프사이클을 편리하게 이용할 수 있는 useEffect 등이 있습니다.

 

이제 본격적으로 리액트 훅들에 대해 공부해보도록 하겠습니다.

그 전에 주의할 점 짚고 시작하겠습니다.

  • JSX 구문이 컴포넌트를 구분할 수 있도록 컴포넌트 이름의 앞 첫글자는 무조건 대문자로 작성해야한다.
  • 컴포넌트에서 return 구문 아래에는 하나의 태그만 존재해야한다.

 

1. useState

먼저 state란 컴포넌트가 가질 수 있는 상태를 말합니다. useState는 이러한 state를 간편하게 생성하고 값을 설정할 수 있도록 해줍니다. 

import {useState} from 'react'

export default function MyComponent () {
	
    // js
    const [state, setState] = useState(초기값);
    
    // ts
    const [state, setState] = useStatr<타입 지정>(초기값);
    // number, string, boolean, number[], string[] 등등
    
	return <></>
}

위와 같이 import를 해주고 초기값과 값을 가지고 있는 state, 값을 설정할 수 있는 setState를 지정해줍니다.

그리고 여기서 중요한 점은 state 값을 변경할 경우 해당 컴포넌트는 재렌더링 된다는 것입니다.

 

또한 state를 변경할 때는 무조건 setState를 사용해야 합니다. 만약에 state가 배열이나 객체일 경우에는 스프레드 연산자를 이용해 state를 복사한 후 값을 변경하고 복사한 녀석을 setState의 인자값으로 넣어주면 됩니다. 

import {useStat} from 'react'

export default function MyComponent () {
	
    const [stateArr, setStateArr] = useState<string[]>(["깁밥", "콩나물", "완두콩"]);
    const [state, setState] = useState<string>("");
	
    const deleteState = (state:string) => {
    	// state 값 복사
        let copy:string[] = [...state];
        
        // 삭제할려는 데이터가 아닌 요소만 가진 배열 만들기
        const delete:string[] = copy.filter((data)=>{
        	return data !== state;
        })
        
        setState(delete);
    }
	
    // 이전 값에 간단한 추가를 하는 경우에는 
    // setState 내부에서 콜백 함수를 사용하여 편리하게 작업할 수 있다.
    const updataData = () => {
    	setStateArr((prev)=>{
        	return [state, ...prev];
        })
    }
    
    
	return (
    <div>
    	<input type='text' value={state} onChange={(e)=>{setState(e.target.value)}}
 		<button onClick = {updateData}>업데이트</button>   	
    	
        {stateArr && stateArr.map((state, index)=>{
        	return <p key={index} onClinck={() => deleteState(state)}>{state}</p>
        })}
    </div>
    )
}

 

 

 

 

그리고 state에 들어가는 초기값이 어떤 무거운 작업을 시행한 후 반환되는 값이라면, 처음 렌더링 될때 한번만 작업을 시행하는 것이 좋습니다. 하지만 그냥 useState(무거운작업함수) 이렇게 넣어버리면 state가 변경될때마다 무거운 작업이 수행되기 때문에 콜백 함수를 사용하여 처음 렌더링 될때만 state의 초기값을 설정하는 함수를 작동시키는 것이 좋습니다.

import {useState} from 'react'

const havyWork = () => {
	console.log('무거운 작업');
    return []
}


export default function MyComponent () {
	
    const [state, setState] = useState(() => {
    	return havyWork
    });
    

	return (
    	<div>
        	<p> {state} </p>
    	</div>
    )
}

 

 



 

2. useEffect

리액트의 라이프 사이클을 이용할 수 있도록 도와주는 Hook입니다.

리액트의 라이프 사이클을 크게 3개로 나눌 수 있는데 Mount(처음 렌더링) > Update(재렌더링) > Unmount(화면에서 사라짐)입니다. useEffect를 통해 원하는 시점에 원하는 작업을 수행할 수 있습니다.

 

1. 처음 렌더링 될때

useEffect(()=>{
	//원하는 작업
},[])

 

2. 렌더링이 될때마다 실행

useEffect(()=>{
	//원하는 작업
})

 

3. 특정 state가 변경될 때

useState(()=>{
	//원하는 작업
},[특정 값])

 

여기서 특정 값을 작성하는 배열은 '의존성 배열'이라고 부릅니다. 이러한 useEffect에서의 clean up에 대해 한 번 알아봅시다. clean up은 useEffect를 통해 특정 작업을 한 후 해당 작업에 대한 정리 작업을 실행하는 것을 이야기합니다.

 

여기서 의존성 배열에 아무것도 존재하지 않는 다면, 컴포넌트가 마운트 됐을때 부수 작업이 실행되고, 언마운트될때 정리 작업이 실행됩니다.

import {useEffect} from 'reat';


export default function Timer () {
	
    useEffect(()=>{
    	// 부수 작업
        const timer = setInterval(()=>{
        	console.log("타이머 실행 중");
        }, 1000);
        
        // useEffect에서 return을 사용하면 해당 컴포넌트가 Unmount 됐을 때 실행이 됩니다.
        return () => {
        	// 정리 작업
            clearInterval(timer);
            console.log("타이버 종료")
        }
    },[]);
    
	return (
    	<div>
        	<span> 타이머 컴포넌트여 </span>
        </div>
    )
}

 

만약에 의존성 배열에 특정 값이 존재한다면, 해당 값이 처음 변경 되었을 때는 부수 작업을 실행하고, 다음 작업부터 기존 state에 대한 정리 작업을 진행한 후 부수 작업을 진행합니다.

 

그리고 브라우저 종속성이 있는 작업을 진행하는 경우 useEffect로 묶는데 그 이유는 리액트가 가상 DOM을 사용하기 때문에 직접적인 DOM 조작은 렌더링 이후에 이루어져야합니다. useEffect는 렌더링 후에 실행되기 때문에 브라우저 종속성이 있는 작업을 실행할 때 useEffect로 감싸는 것입니다.

 

useEffect의 실행 시점

  • 초기 렌더링 후: 컴포넌트가 처음 렌더링된 직후에 useEffect의 부수 효과 코드가 실행됩니다.
  • 리렌더링 후: 의존성 배열에 포함된 값이 변경되어 컴포넌트가 리렌더링될 때, 이전의 정리 함수가 실행된 후에 새로운 부수 효과 코드가 실행됩니다.
  • 언마운트 직전: 컴포넌트가 언마운트되기 직전에 useEffect의 정리 함수가 실행됩니다.

 



 

3. useRef

ref 객체를 반환해주는 Hook입니다. useRef를 이용해 설정하는 초기값은 ref 객체의 current에 저장됩니다.

이러한 ref는 크게 두 가지 용도로 사용됩니다.

 

1. 저장 공간

ref의 값을 변경해도 리렌더링 되지 않기 때문에  컴포넌트 내의 변수들의 값을 유지한 채 특정 값을 변경하고 저장하고 싶을 때 유용합니다. 그리고 컴포넌트가 렌더링 되어도 ref의 값은 유지됩니다.  즉, ref의 값은 컴포넌트가 마운트 된 이후 언마운트될 때까지 값이 유지되는 것입니다.

 

버튼을 누르면 값을 증가시켜 주는 예제를 통해 차이를 비교해보겠습니다.

import {useState, useRef} from 'react';

export default function CountComponent () {
	
    const [state, setState] = useState<number>(0);
    const ref = useRef<number>(0)
    // 요소를 참조할 경우 const ref = useRef<HTML요소이름Element>(null);
    
    console.log("렌더링 ")
    
    return (
    <div>
    	<p> state: {state} </p>
        <p> ref : {ref} </p>
        <button onClick = {() => { setState( state + 1) }}> state 값 올리기 </button>
    	<button onClick = {() => { ref.current = ref.current + 1} }> ref 값 올리기 </button>
    </div>
    )

}

위의 코드를 실행하고 'state 값 올리기' 버튼을 올리면 "렌더링"이 콘솔 창에 버튼을 누를 때마다 뜨고 변경된 값이 바로바로 화면에 적용되어 보일 것입니다, 하지만 'ref 값 올리기'를 클리하면 렌더링이 일어나지 않기 때문에 ref의 값이 버튼을 클리할 때마다 1씩 증가하고 있지만 그 변화가 화면에 반영되어 나타나지는 않습니다. 이 때 'state 값 올리기' 버튼을 클릭할 경우 증가한 ref의 값도 반영되어 화면에 표시될 것입니다.

 

때문에 값이 자주 바뀌는 경우 ref를 사용하는 게 성능상 유리할 것입니다. 그리고 변화를 감지해야 하지만 변화를 감지할 때 렌더링을 발생시키지 않아야하는 경우에 사용하기 유용할 것입니다.

 

 

2. DOM 요소 접근

ref를 지정한 요소에 접근할 수 있고, ref.current.focus(), ref.current.value, ref.current.style.backgroundColor, ref.current.checked 등 기존 document.querySelector로 불러와서 할 수 있는 작업들을 동일하게 할 수 있습니다.


이런식으로 사용하기 위해선 태그의 ref 속성으로 useRef를 넣어주면 DOM 요소에 쉽게 접근할 수 있는 것입니다.

이러한 ref를 사용하는 다양한 예제를 보면서 이해하면 좋을 것 같습니다.

 

1. 렌더링 될 때, 특정 input 요소에 포커싱

import { useRef, useEffect } from 'react';

export default function FocusInput() {
  const inputRef = useRef(null);

  useEffect(() => {
    inputRef.current.focus(); // input 요소에 포커스를 설정
  }, []);

  return <input ref={inputRef} type="text" />;
}

 

 

2. DOM 요소의 값을 읽고 설정

import { useRef } from 'react';

// input의 value 가지고 와서 설정하기
export default function ReadWriteInput() {
  const inputRef = useRef(null);

  const handleClick = () => {
    alert(inputRef.current.value); // input 요소의 현재 값을 읽음
    inputRef.current.value = 'Hello, World!'; // input 요소의 값을 설정
  };

  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={handleClick}>Show and Set Value</button>
    </div>
  );
}


// input이 체크되어 있는지 확인하고 변경하기
export default function RadioComponent() {
  const radioRef = useRef(null);

  const handleClick = () => {
    // 현재 체크 상태를 읽음
    console.log('Radio button is checked:', radioRef.current.checked);

    // 체크 상태를 설정
    radioRef.current.checked = !radioRef.current.checked;
  };

  return (
    <div>
      <input ref={radioRef} type="radio" name="example" />
      <button onClick={handleClick}>Toggle Radio Button</button>
    </div>
  );
}

 

 

3. DOM 요소 스타일 변경

import { useRef } from 'react';

export default function ChangeStyle() {
  const divRef = useRef(null);

  const handleClick = () => {
    divRef.current.style.backgroundColor = 'yellow'; // div 요소의 배경색 변경
  };

  return (
    <div>
      <div ref={divRef} style={{ width: '100px', height: '100px', backgroundColor: 'blue' }}></div>
      <button onClick={handleClick}>Change Color</button>
    </div>
  );
}

 

 

4. 스크롤 조작

import React, { useRef } from 'react';

export default function ScrollBox() {
  const boxRef = useRef(null);

  const handleScrollToBottom = () => {
    boxRef.current.scrollTop = boxRef.current.scrollHeight; // 스크롤을 가장 아래로 이동
  };

  return (
    <div>
      <div
        ref={boxRef}
        style={{ width: '200px', height: '100px', overflow: 'auto', border: '1px solid black' }}
      >
        <p>Some content here...</p>
        <p>More content here...</p>
        <p>Even more content...</p>
        <p>And more...</p>
        <p>More and more...</p>
        <p>Last content...</p>
      </div>
      <button onClick={handleScrollToBottom}>Scroll to Bottom</button>
    </div>
  );
}

 

 

5. 타미어 및 애니메이션 제어

import React, { useRef, useEffect } from 'react';

export default function TimerComponent() {
  const timerIdRef = useRef(null);

  useEffect(() => {
    // setInterval을 사용하여 1초마다 로그를 찍는 타이머 설정
    timerIdRef.current = setInterval(() => {
      console.log('Timer tick');
    }, 1000);

    return () => {
      // 컴포넌트 언마운트 시 타이머 제거
      clearInterval(timerIdRef.current);
    };
  }, []);

  return <div>Timer is running. Check the console for ticks.</div>;
}

이렇게 하는 이유는 컴포넌트가 다시 렌더링되어도 해당 타이머 및 애니메이션이 유지될 수 있도록 하기 위함입니다.

 

위와 같이 useRef를 이용해 다양한 작업들을 할 수 있습니다. 해당 글을 여기서 마무리하도록 하고 다음 글에 이어서 useContext, useMemo,  useCallback, useReducer에 대해 정리하겠습니다.

 

서비스 워커란?

 서비스 워커란 웹 애플리케이션을 네이티브 앱처럼 로컬에 설치하여 실행 성능을 향상 시키는 역할을 하고, 전통적인 웹 개발 모델을 확장하는 개념으로 사용됩니다.  즉, 웹 애플리케이션에 서비스 워커를 추가하는 것은 애플리케이션을 progressive Web App으로 전환하는 과정 중 하나의 단계라고 할 수 있습니다.

 

 조금 더 자세히 설명드리자면, 서비스 워커는 애플리케이션의 캐시를 관리하기 위해 웹 브라우저가 백그라운드에서 실행하는 스크립트입니다. 이러한 서비스 워커는 네트워크와 브라우저 사이의 프록시 서버 역할을 하며, 애플리케이션에서 보내는 HTTP 요청을 모두 인터셉트해서 해당 요청에 대한 응답을 직접 보낼 수 있습니다. 그래서 이미 응답을 받은 요청이 있다면, 해당 응답을 로컬에 캐싱해 뒀다가 재사용할 수 있습니다.이러한 서비스 워커로 푸시 알림이나 오프라인 상태에서는 백그라운드 동기화 같은 기능을 구현할 수 있습니다.

 

 프록시 기능은 fetch와 같은 API뿐만 아니라 HTML 파일이 참조하는 리소스에도 적용할 수 있습니다. 웹 페이지와 별개로 생명주기를 갖고 동작하며, 보안상의 이유로 HTTPS와 localhost에서만 실행됩니다.

 

 

서비스 워커 적용하기

 public 폴더에 sw.js 파일을 만들어 어떤 작업을 할 것인지 정의해주고, useEffect 훅으로 감싸준 후 등록 가능한 애플리케이션에 대해서 설정한 sw.js 파일을 등록하면 됩니다. 브라우저에 종속적이기 때문에 useEffect 훅으로 감싸줘야 합니다.

정상적으로 등록이 된다면 개발자 도구에서 확인이 가능합니다. (아래에서 이미지와 함께 조금 더 자세히 설명드리겠습니다.) 이러한 서비스 워커 캐싱은 몇가지 패턴이 존재하는데 상황에 맞는 패턴으로 개발을 진행하면 됩니다.

출저 : https://80000coding.oopy.io/2c8097c6-117f-41b4-ae65-2bd0beec5f18

 제가 프로젝트에 적용하고자 하는 패넡은 Cache, falling back to Network 입니다. 즉 캐싱한 응답이 있다면 캐싱된 데이터를 보내주고, 그렇지 않다면 원래의 요청을 보내는 것입니다.

 

이제 직접 프로젝트에 적용해보겠습니다.


1. public 폴더에 sw.js 파일을 만들어 어떤 작업을 할 것인지 정의해 줍니다.

// public/sw.js

const cacheName = 'useServiceWorker'
const cacheList = []

// fetch가 일어날 때
self.addEventListener('fetch', event => {
  // 응답을 가져와서 수정
  event.respondWith(
    
    // request에 대한 response를 캐싱했는지 확인
    caches.match(event.request).then(response => {
      
      // 캐싱했다면 캐싱한 데이터 반환
      if (response) {
        return response
      }

      const fetchRequest = event.request.clone()
			
			// 캐싱한 데이터가 없다면 원래 요청을 보내주기
      return fetch(fetchRequest).then(response => {
        if (!response || response.status !== 200 || response.type !== 'basic') {
          return response
        }

        const requestUrl = event.request.url
        const responseToCache = response.clone()
				
				// POST 요청에 대한 응답이나 chrome extension에 대한 응답은 캐싱하지 않음
        if (
          !requestUrl.startsWith('chrome-extension') &&
          event.request.method !== 'POST'
        ) {
          // 캐시에 요청에 대한 응답 저장
          caches.open(cacheName).then(cache => {
            cache.put(event.request, responseToCache)
          })
        }
				
				// 요청 반환
        return response
      })
    }),
  )
})

 

 

2. 서비스 워커 등록 컴포넌트를 만들어줍니다.

 최상위 컴포넌트가 마운트 될 때 서비스 워커를 등록해 주면 되는데 위에서 언급했듯이 서비스워커는 브라우저에 종속적이기 때문에 useEffect로 묶어서 사용해 줘야 합니다. 처음에 적용할 때 서비스워커가 아예 작동하지 않는 문제가 있었는데 이 문제도 짚어가면서 등록한 과정을 작성하겠습니다.

 

  • 처음 작성했던 등록 코드

 최상위 컴포넌트인 layout.tsx는 서버 컴포넌트를 유지 시켜 줘야 하기 때문에 따로 컴포넌트를 만들어서 넣었습니다.

// /utils/RegisterServiceWorker.tsx

'use client'

import {useEffect} from 'react'

export default function RegisterServiceWorker () {
	useEffect(()=>{
		if('serviceWorker' in navigator) {
			window.addEventListener('load', () => {
				navigator.serviceWorker.register('/sw.js')
				.then(registration => {
          console.log('SW registered: ', registration)
        })
        .catch(registrationError => {
	        console.log('SW registration failed: ', registrationError)
        })
			})
		}
	},[])
	
	// 렌더링하는 요소는 없기 때문에 null을 반환해준다.
	return null
}

 

 이 코드를 작성하면서 문제를 깨달았어야 했는데, 그러지 못했습니다.. 하핳

원래window.addEventListener(’load’, () ⇒ {})를 사용한 이유는 페이지 로드가 완료된 후에 서비스워커를 등록함으로써 페이지 로드 속도에 영향을 미치지 않을려고 작성했습니다. 하지만 이로 인해 서비스워커가 등록되지 않았습니다.

 

 왜냐하면 useEffect(()⇒{},[]) 내에서 window.addEventListener(’load’, () ⇒ {내부코드})를 사용할 경우 내부코드가 실행되지 않기 때문이었습니다.

 

 이 이유를 찾기 위해 챗지피티한테 물어보기도 하고 구글링도 하고 직접 다른 코드들을 작성하며 도달한 결론은 다음과 같습니다. 각 이벤트가 호출되는 기준 때문이라고 생각합니다. load 이벤트는 모든 리소스가 다운로드 된 다음에 이벤트가 호출되는데 리액트 기준에서는 load 이벤트가 일어나지 않았기 때문에 window로 load 이벤트 이후에 서비스 워커를 등록할려고 한 작업은 실행되지 않은 것입니다. 여기서 확실하게 파악하지 못한 부분은 컴포넌트가 마운트 되기 이전에 이미 페이지 로드가 되었다고 판단하여 이벤트가 실행되지 않는 것인지 컴포넌트가 마운트된 이후에 업데이트가 계속 발생하기 때문에 페이지가 로드되었다고 판단하지 않는 것입니다. 하지만 DOM 트리를 완성하는 시점에 이벤트를 호출할 수 있는 DOMContentLoaded를 사용할 경우 서비스 워커 등록이 아주 잘되는 것을 확인하고, 후자가 맞지 않나라고 생각하고 있습니다. 이 부분은 더 찾아보고 공부해야할 것 같습니다.

 

그리고 사실 따로 이벤트를 설정할 필요 없이 컴포넌트가 렌더링 되는 시점에 서비스 워커를 등록하면 문제없이 잘 실행됩니다. 여기서 주의해야할 점은 navigator.serviceWorker.register는 Promise 객체를 반환하기 때문에 .then이나 catch를 사용하거나 async/await을 사용해야 합니다. 저는 가독성이 좋은 async/await을 사용했습니다.

 

  • 실험 코드
// /utils/RegisterServiceWorker.tsx

'use client'

import { useEffect } from 'react'

export default function RegisterServiceWorker() {
  useEffect(() => {
    console.log(navigator.serviceWorker)

    if ('serviceWorker' in navigator) {
      const registerServiceWorker = async () => {
        try {
          const registration = await navigator.serviceWorker.register('/sw.js')
          console.log('SW registered: ', registration)
        } catch (error) {
          console.log('SW registration failed: ', error)
        }
      }
      
      registerServiceWorker() // 실행 됨

      window.addEventListener('DOMContentLoaded', () => {
        registerServiceWorker() // 실행 됨
      })

      window.addEventListener('load', () => {
        console.log('페이지 로드 완료')
        registerServiceWorker() // 실행 안됨
      })
    }
  }, [])

  return null
}

 

  • 최종 코드
// /utils/RegisterServiceWorker.tsx

'use client'

import { useEffect } from 'react'

export default function RegisterServiceWorker() {
  useEffect(() => {
    console.log(navigator.serviceWorker)

    if ('serviceWorker' in navigator) {
      const registerInit = async () => {
        const registration = await navigator.serviceWorker.register('/sw.js')
        registration.waiting?.postMessage({ type: 'SKIP_WAITING' })
      }
      registerInit()

      // 그냥 naviaator.serviceWorker.register('/sw.js') 사용하는 경우
      //   navigator.serviceWorker
      //     .register('/sw.js')
      //     .then(registration => {
      //       console.log('Service Worker registered: ', registration)
      //     })
      //     .catch(registrationError => {
      //       console.log('SW registration failed: ', registrationError)
      //     })
    }
  }, [])

  return null
}
// /app/layout.tsx

import type { Metadata } from 'next'
import ReactQueryProvider from '../utils/provider/ReactQueryProvider'
import RegisterServiceWorker from '@/utils/client/RegisterSeviceWorker'
import WebView from '@/Components/WebView/WebView'
import { WeatherProviderByContext } from '../../contexts/WeatherContext'
import './globals.css'

export const metadata: Metadata = {
  title: 'Create Next App',
  description: 'Generated by create next app',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="ko">
      <title>옷늘날씨</title>
      <body>
        <ReactQueryProvider>
          <RegisterServiceWorker />
          <WebView>
            <WeatherProviderByContext>{children}</WeatherProviderByContext>
          </WebView>
        </ReactQueryProvider>
      </body>
    </html>
  )
}

 

 

3. 서비스 워커 잘 작동되는지 확인

개발자 도구의 application으로 들어가서 확인할 수 있습니다.

캐시된 데이터 크기는 Storage에서 확인 가능합니다.


위와 같이 적용하고 배포까지 했는데 문제가 발생했습니다. 서비스 워커를 적용한 이후 서비스 워커에서 데이터를 가져오는 방식이 기존 fetch를 이용한 방식보다 더 느리다는 것이었습니다. 코드를 보면서 뭐가 문제인지 알아보겠습니다.

 

// public/sw.js

const cacheName = 'useServiceWorker'
const cacheList = []

// fetch가 일어날 때
self.addEventListener('fetch', event => {
  // 응답을 가져와서 수정
  event.respondWith(
    
    // request에 대한 response를 캐싱했는지 확인
    caches.match(event.request).then(response => {
      
      // 캐싱했다면 캐싱한 데이터 반환
      if (response) {
        return response
      }

      const fetchRequest = event.request.clone()
			
			// 캐싱한 데이터가 없다면 원래 요청을 보내주기
      return fetch(fetchRequest).then(response => {
        if (!response || response.status !== 200 || response.type !== 'basic') {
          return response
        }

        const requestUrl = event.request.url
        const responseToCache = response.clone()
				
				// POST 요청에 대한 응답이나 chrome extension에 대한 응답은 캐싱하지 않음
        if (
          !requestUrl.startsWith('chrome-extension') &&
          event.request.method !== 'POST'
        ) {
          // 캐시에 요청에 대한 응답 저장
          caches.open(cacheName).then(cache => {
            cache.put(event.request, responseToCache)
          })
        }
				
				// 요청 반환
        return response
      })
    }),
  )
})

해당 코드를 보면 캐싱한 데이터가 없을 경우 이벤트의 request를 클론하여 원래 요청을 진행하는데요 생각해보면 이 방식은 기존 요청이 있는 상태에서 다시 요청하는 방식이기 때문에 불필요한 중복 요청을 발생시키고 있습니다. 이러한 중복 요청을 제거하겠습니다.

 

수정 코드

// /public/sw.js

const cacheName = 'useServiceWorker'

self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request).then(response => {
      if (response) {
        return response
      }

      // request 클론 과정 삭제
      // const fetchRequest = event.request.clone()

      return fetch(event.request).then(response => {
        if (!response || response.status !== 200 || response.type !== 'basic') {
          return response
        }

        const requestUrl = event.request.url
        const responseToCache = response.clone()

        if (
          !requestUrl.startsWith('chrome-extension') &&
          event.request.method !== 'POST'
        ) {
          caches.open(cacheName).then(cache => {
            cache.put(event.request, responseToCache)
          })
        }

        return response
      })
    }),
  )
})

 

해당 코드로 배포 후 테스트 진행해보겠습니다. 성공적으로 서비스 워커를 이용해 fetch로 가져오는 데이터를 캐싱할 수 있었고, 기존 로딩 시간을 97% 단축했습니다.

 

기존 리소스 로딩 시간

 

서비스 워커 적용 후 리소스 로딩 시간 (이미지 포맷도 변경)

 

이상입니다,

깃허브에 PR을 잘못했는데 Merge까지 해버린 경우가 있어서 해당 PR을 제거할려고 시도한 경험이 있습니다.

이러한 PR을 완전히 제거하는 방법은 생각보다 간단합니다.

 

https://support.github.com/request

 

해당 링크로 들어가서 다음 과정을 수행하면 됩니당.

 


잘못 올린 PR 완전히 제거하기

 

1. '내가 가지고 있거나 제어하는 리포지토리에서 데이터 제거하기'를 클릭합니다. 

 

 

2. 끌어오기 요청 제거하기를 클릭합니다.

 

3. 여러개의 PR을 제거하고 싶다면 Multiple, 하나의 PR을 제거하고 싶다면 Single을 선택하면 됩니다.

 

4. 이후 해당 질문이 왔을때, PR을 제거할려고 하는 repository의 주소를 입력해줍니다.

 

5. 삭제할려고 하는 PR의 번호를 작성하면 2시간 이내에 삭제가 됩니다.


 

하지만, 저는 위와 같은 메세지를 받았고, PR이 삭제되지 않았습니다.

위의 답변을 보면 알 수 있듯이 삭제할려는 PR과 연관된 브랜치에서 각 PR에 대한 commit을 삭제해줘야 PR을 완전히 삭제할 수 있는데 해당 작업이 시행되어 있지 않았기 때문이었습니다. 이를 위해선 삭제할려는 PR이 있는 repository를 클론하고 git-filter-repo BFG Repo-Cleaner 를 이용하여 PR과 연관된 각 브랜치에서 commit을 삭제해줘야합니다.

 

 

git-filter-repo를 이용해 제거한 과정을 정리해볼려고 합니다.

 


1. repository 클론하기

git clone --mirror https://github.com/your-repository.git
cd your-repository.git

 

--mirror를 사용하는 주요 이유는 다음과 같습니다:

  1. 모든 브랜치와 태그 포함: 전체 저장소의 모든 브랜치와 태그를 포함하여 완전한 복사본을 만듭니다. 이는 특정 커밋을 모든 브랜치와 태그에서 제거하려는 경우 필수적입니다.
  2. 원격 참조 유지: 원격 저장소의 모든 참조를 로컬에 그대로 복사하므로, 원격 저장소의 구조를 로컬에서 정확히 재현할 수 있습니다.
  3. 작업 편의성: git filter-repo와 같은 도구를 사용하여 모든 브랜치와 태그에서 커밋을 제거하는 작업을 한 번에 수행할 수 있습니다

 

2. git-filter-repo 설치

pip install git-filter-repo

 

 

3. 제거할려고 하는 커밋 확인 

git log

 를 이용해 제거할려는 커밋의 해시를 알아냅니다.

 

4. 제거할려는 커밋의 해시를 입력해 해당 커밋 삭제

git filter-repo --commit-callback 'if commit.hash == b"{커밋 해시}": commit.ignore = True'

# 예시
git filter-repo --commit-callback '
def callback(commit, metadata):
    if commit.original_id == b"27d6475280e0d347d8bc88e4edcb149bad3fb248":
        commit.ignore = True
'

 

5. 깃허브 저장소 주소 연결

git remote add https://github.com/your-repository.git

 

6. push

git push --force --all

 


위의 방법으로 삭제할려고 하는 PR과 관련된 커밋을 각 브랜치에서 삭제할 수 있습니다. 이렇게 처리한 뒤에 문의할 때 제공받은 티켓에서 댓글로 삭제했다고 연락하면 담당자가 PR을 완전히 제거해 줍니다. 이상입니다. 글 읽어주셔서 감사합니다.

개발자는 커뮤니케이션 능력이 굉장히 중요하다고 생각합니다. 그렇기에 개발하게될 제품에 대해서 다른 부서에 잘 설명할 수 있어야합니다. 이를 도와주는 PRD에 대해 소개하고 싶습니다.

 

 

PRD란 무엇인가?

PRD(Product Requirements Document)는 제품을 만들거나 업데이트하기 위해 기능을 기획하는 단계에서 요구사항을 개괄적으로 설명하는 문서입니다. 이는 제품을 개발하는 전체 프로세스에서 매우 중요하게 사용됩니다.

 

기획 단계에서는 제품의 비전과 전략을 정의하고,

디자인 단계에서는 제품이 사용자 요구 사항을 충족하고 있는지 확인합니다.

다음으로 개발 단계에서는 기능의 목적과 구현을 확인하고,

테스트 단계에서는 제품이 기술 요구 사항을 충족하는지 확인합니다.

마지막으로 출시 단계에서는 제품이 시장 혹은 사용자의 요구 사항을 충족하는지 확인하는 데 사용합니다.

 

이렇듯 제품을 개발하는 전반적인 과정에서 메뉴얼의 역할을 해주는 중요한 문서입니다.

 

PRD 작성 방법

구글의 프로덕트 매니저인 Omar Eduardo Fernández가 정리한 PRD 작성하는 방법을 작성하겠습니다.

 

1. 사용자 문제 및 비즈니스 우선순위 결정의 근거를 포함해 주세요.

개발할 기능의 우선순위를 명확하게 정해줘야합니다. 이를 통해 해결 방법에 따른 기능과 디자인을 평가하고 복잡성과 사용자 가치의 균형을 맞추는 데 도움이 됩니다.

그리고 이 과정에서 개발 프로세스를 고려하여 사용자 문제와 개발 목표를 검토하고, 해결 방안에 대한 내용을 작성해야 합니다. 해당 내용에는 다음이 포함됩니다. 

  • 사용자 조사 결과
  • 다양한 옵션에 대한 토론과 평가
  • 기술적 한계
  • UX 고려 사항
  • 리소스 및 예산
  • 타임라인 요구 사항

그리고 이 내용들은 이해관계자들과 의사 결정권자와 공유하고 진행이 되어도 괜찮은지 의견을 받아야합니다.

 

2. 해결 방안에 대한 세부 정보를 추가하세요.

PRD에는 개발 및 디자인 프로세스를 고려하여 제안된 해결 방안에 대한 세부 정보가 작성되어 있어야 합니다.

해당 정보에는

  • 사용자 조사 결과를 확인할 수 있는 문서
  • 다양한 옵션이나 아이디어에 관해 토론한 회의 내용
  • 기술적 한계
  • UX 고려 사항
  • 리소스 및 예산 제약
  • 타임라인 요구 사항

등이 포함됩니다. 또한 다른 팀에 대한 종속성 및 관련 링크도 포함되어야 하며, 같이 협업하는 팀원들이 PRD를 이해할 수 있도록 작성되어야 합니다.

 

3. UX 및 개발 설계 문서를 참조하세요.

PRD는 구축해야할 기능과 목적을 명확히 하고 개발, UX 디자인 문서를 포함하여 기능이 구축되는 방식을 문서화합니다.

이 단계에서는 고려된 옵션에 대한 피드백을 제공하고 PRD에 설정된 우선순위를 다시 확인합니다.

설계를 마무리할 때, 제품을 만드는 구성원들과 이야기하여 우선순위를 명확히 하고 사용자 가치와 복잡한 기능의 균형을 맞추는 부분을 설명할 수 있어야 합니다.

 

4. PRD를 업데이트하여 기능 범위를 결정하세요.

PRD를 업데이트하여 기능 범위에 대한 결정을 내리고,

UX 및 개발 설계 문서와 중복되지 않도록 합니다.

  • 중요한 결정과 절충안
  • 기술적 고려 사항
  • 다른 팀에 대한 종속성
  • 사용자 개인정보보호 or 법적 문제 명시

위의 내용을 작성하여 행후 중요한 기능을 활성화하기 위해 특정 버전의 기능을 구축하기로 했다면 이 경우에도 기록합니다.

 

5. 다른 사람들과 공유하여 의견을 구하세요.

PRD 필수 변경 사항을 자주 공유하여 다른 사람들의 의견을 구하는 것이 중요합니다.

PRD에 대한 의견을 구하기 위해 관련 이해관계자와 상호작용하고, 업데이트 사항을 빠르게 공유하여 누락된 사항을 고려할 수 있도록 합니다. 이를 통해 PRD의 결정된 내용과 근거를 빠르게 확인하고, 의견을 나눠 장단점을 고려할 수 있습니다.

 

6. 개발 중에도 PRD를 계속 업데이트 하세요.

개발 중에 기능 범위와 타임라인 사이에서 절충안을 찾아야할 경우가 있기에 PRD를 계속 업데이트하고 타임라인 및 범위에 대한 중요한 업데이트를 다른 팀과 공유해야합니다. 이를 통해 팀은 능동적으로 계획하고 빠르게 문제를 해결할 수 있습니다.

 

 

이렇게 총 6가지의 내용을 정리해 봤습니다. 하지만 위의 내용만 가지고는 어떤 형식으로 PRD를 구현해야 하는지 알기 어렵습니다. 바로 PRD의 구성요소를 알아봅시다.

 

PRD의 구성 요소 7

이는 개발 범위와 회사마다 차이가 있으므로 반드시 모든 요소를 포함해야하는 것은 아닙니다.

1. 제품 개요(요약과 배경)

  • 제품의 목적
  • 해결해야 할 문제에 대한 요약
  • 왜 이 문제를 해결해야 하는지 근거 작성

기획, 개발의 중요성을 강조할 수 있는 근거 데이터(설문조서, 인터뷰, 지표)를 참고하면 좋습니다.

최근 몇 달간 사용성에 대한 이슈가 있거나, VOC(고객의 소리) 내용에 반복적으로 나오는 키워드들을 데어터로 정리할 수도 있습니다. 혹은 신규 개발의 경우 시장의 상황이나 변화, 경쟁사의 제품의 성장 등을 추가할 수 있습니다.

 

2. 비즈니스 목표 혹은 지표

  • 제품을 개발하는 이유
  • 기대되는 비즈니스 성과
  • 제품이 달성해야하는 비즈니스 목표
  • 사용자가 제품을 사용함으로써 달성하고자 하는 비즈니스 결과

를 작성합니다. 현재 시점의 데이터와 예상 가능한 데이터를 비교할 수 있도록 작성하는 것이 좋습니다.

 

1. 비즈니스 목적 분석 : 먼저 비즈니스가 달성하고자 하는 목표를 분석합니다. 예를 들어, 매출 증가, 시장 점유율 확대 등이 될 수 있습니다.

 

2. 키 성과 지표(KPI) : 비즈니스 목표에 따라 측정 가능한 KPI를 식별합니다. 예를 들어, 매출 증가를 목표로 한다면 매출 증가율, 고객 당 평균 거래 금액, 신규 고객 유입 비율 등을 KPI로 사용할 수 있습니다.

 

3. 목표 수치 설정 : 각 KPI에 대해 목표 수치를 설정합니다. 예를 들어, 매출 증가율을 10%로 설정할 수 있습니다.

 

3-1. 핵심 고객/사용자 정의

고객 및 사용자는 제품 기획에서 항상 중심이 됩니다. 만들 제품을 사용할 사용자에 대해 정의해야합니다.

제품에 다양한 기능이 있을수록 사용자는 세분화할 수 있습니다. 신규 개발이 되었을 때 갈증이 해소될 사용자가 어떤 문제와 어려움을 겪고 있는 사용자인지 분명하게 서술해야합니다. 아래와 같은 방법으로 사용자를 정의하는 방법도 있습니다.

 

3-2. 핵심 사용자 여정(CUJ) / 사용자 스토리

CUJ

신규 개발 후 정의한 사용자가 목표한 달성할 수 있는지 작성합니다. CUJ는 사용자의 문제와 필요에 집중하여, 사용자가 제품 또는 서비스를 사용할 때 가장 중요한 요소를 파악하고 해결하는 데 도움이 됩니다.

 

이 떄 중요한 건 사용자 요구에 집중하며, 해결 방안에 집착하지 않는 것입니다. 특정 해결 방안이나 기능에만 집중하게 된다면, 근본적인 문제 해결에서 멀어질 수 있습니다. 이는 문장으로 작성할 수도 있지만, 사용자 여정 지도와 같은 시각화된 자료로 만들기도 합니다.

 

사용자 스토리

사용자 스토리는 사용자가 제품 또는 거비스를 이용할 때 생기는 상황, 목적, 요구사항 등을 간결하고 명확하게 표현한 문장입니다. 다음과 같이 작성할 수 있습니다.

“(고객/사용자 유형)은 (목적/목표)를 위해 (필요/욕구)를 원한다” 
”(고객/사용자 유형)쇼핑을 자주 하는 나는, (목적/목표)결제 정보를 저장하여 
매번 새롭게 정보를 다시 입력할 필요 없이, (필요/욕구)빠르게 결제할 수 있기를 원한다.”

이렇게 작성된 유저 스토리는 제품 또는 서비스를 개발할 때 사용자 중심으로 기능을 설계하고 개발할 수 있도록 도와줍니다.

 

5. 기능적 요구 사항

기능적 요구 사항은 아래 내용과 같이 우선순위를 표시하여 작성합니다. 제품 개발을 하는 팀과 함꼐 상의하며 작성하는 것이 좋습니다.

  1. 먼저 대상 사용자를 정합니다. 예를 들어, 음식 주문 앱의 사용자라면 “음식을 주문하려는 사용자”가 됩니다.
  2. Must Have는 반드시 구현되어야 하는 기능으로, 해당 사용자가 해당 기능 없이는 앱을 사용할 수 없다는 것입니다. 예를 들어, 음식 주문 앱에서는 메뉴를 선택하고 주문을 결제하는 것이 Must Have가 될 수 있습니다.
  3. Should Have는 반드시 필요한 것은 아니지만, 해당 사용자가 매우 필요하다고 생각하는 기능입니다. 예를 들어, 음식 주문 앱에서는 메뉴를 검색할 수 있는 기능이 Should Have가 될 수 있습니다.
  4. Could Have는 선택적인 기능으로, 해당 사용자가 원한다면 사용할 수 있는 기능입니다. 예를 들어, 음식 주문 앱에서는 특정 음식점의 할인 정보를 확인할 수 있는 기능이 Could Have가 될 수 있습니다.

 

 

6. 프로젝트 일정 및 배포 계획

제품 개발 및 출시 일정을 작성합니다.

 

7. 기타 참고 문서

와이어 프레임, 스토리보드, UX 리서치 결과 등등이 첨부되면 좋습니다. 또한 트렌드를 확인할 수 있는 레퍼런스 자료들도 첨부합니다.

 

이상으로 PRD를 작성하는 기본적인 방법을 알아봤습니다. 이를 모두 다 적을 순 없더라도 프로젝트를 시작하기 전에 참고하여 작성하고 개발을 시작한다면 충분히 도움이 될 것이라고 생각합니다. 긴 글 읽어주셔서 감사합니다!

 

 

 

 

+ Recent posts