학습 내용 정리

카카오 주소찾기 적용, 인풋 첫글자 자동완성 대문자, useState callback 사용하기, 리액트, 타입스크립트 name of undefined에러, withRouter를 통한 history 객체 타입 매칭, 브라우저 새탭에서 열기, 새창에서 열기, 모바일 사파리에서 키보드가 나올때 포지션 깨짐현상, 웹팩에서 public폴더 사용하기, 리액트 다중요소 ref값 붙인 뒤 확인, cra빌드와 사용자정의 경로,

카카오 주소찾기 적용기

카카오 주소검색은 매우 유용하게 사용이 가능하다. 별도의 키를 발급받을 필요가 없고, 사용량에 제한도 없으며 기업용이던 상업적 용도이던 무료로 확인기 가능하며, 도로명 주소, 지번 주소, 영문 주소까지도 모두 확인이 가능하다. PC 및 모바일 웹 환경에서도 지원하게 되어있다. 사용하지 않을 이유가 없다. 처음엔 검색창에 kakao라고 쓰여있어서 카카오 디벨롭 에서 내용을 찾았는데 없었다. 알고보니 다음에 따로 가이드가 있었다.

<script src="https://t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js"></script>

index.html에 스크립트를 하나 추가해준다.
검색창을 띄우는 방법은 다음과 같다.

new daum.Postcode({
  oncomplete: function(data) {
    // 팝업에서 검색결과 항목을 클릭했을때 실행할 코드를 작성하는 부분입니다.
    // 예제를 참고하여 다양한 활용법을 확인해 보세요.
  },
}).open()

가이드 페이지에 주석과 설명이 잘 되어있어서 이해하기 편했다. 일반적으로 검색창을 띄우고 input에 상세값을 맞춰주는 예제는 다음과 같다.

function sample4_execDaumPostcode() {
        new daum.Postcode({
            oncomplete: function(data) {
                // 팝업에서 검색결과 항목을 클릭했을때 실행할 코드를 작성하는 부분.

                // 도로명 주소의 노출 규칙에 따라 주소를 표시한다.
                // 내려오는 변수가 값이 없는 경우엔 공백('')값을 가지므로, 이를 참고하여 분기 한다.
                var roadAddr = data.roadAddress; // 도로명 주소 변수
                var extraRoadAddr = ''; // 참고 항목 변수

                // 법정동명이 있을 경우 추가한다. (법정리는 제외)
                // 법정동의 경우 마지막 문자가 "동/로/가"로 끝난다.
                if(data.bname !== '' && /[동|로|가]$/g.test(data.bname)){
                    extraRoadAddr += data.bname;
                }
                // 건물명이 있고, 공동주택일 경우 추가한다.
                if(data.buildingName !== '' && data.apartment === 'Y'){
                   extraRoadAddr += (extraRoadAddr !== '' ? ', ' + data.buildingName : data.buildingName);
                }
                // 표시할 참고항목이 있을 경우, 괄호까지 추가한 최종 문자열을 만든다.
                if(extraRoadAddr !== ''){
                    extraRoadAddr = ' (' + extraRoadAddr + ')';
                }

                // 우편번호와 주소 정보를 해당 필드에 넣는다.
                document.getElementById('sample4_postcode').value = data.zonecode;
                document.getElementById("sample4_roadAddress").value = roadAddr;
                document.getElementById("sample4_jibunAddress").value = data.jibunAddress;

                // 참고항목 문자열이 있을 경우 해당 필드에 넣는다.
                if(roadAddr !== ''){
                    document.getElementById("sample4_extraAddress").value = extraRoadAddr;
                } else {
                    document.getElementById("sample4_extraAddress").value = '';
                }

                var guideTextBox = document.getElementById("guide");
                // 사용자가 '선택 안함'을 클릭한 경우, 예상 주소라는 표시를 해준다.
                if(data.autoRoadAddress) {
                    var expRoadAddr = data.autoRoadAddress + extraRoadAddr;
                    guideTextBox.innerHTML = '(예상 도로명 주소 : ' + expRoadAddr + ')';
                    guideTextBox.style.display = 'block';

                } else if(data.autoJibunAddress) {
                    var expJibunAddr = data.autoJibunAddress;
                    guideTextBox.innerHTML = '(예상 지번 주소 : ' + expJibunAddr + ')';
                    guideTextBox.style.display = 'block';
                } else {
                    guideTextBox.innerHTML = '';
                    guideTextBox.style.display = 'none';
                }
            }
        }).open();

위와 같은 함수를 onClick내에서 실행시키면 검색창을 띄울 수 있다. 단 위와같이 .open()으로 창을 띄우게 되면 모바일에서는 새창이 떠서 사용자경험 측면에서 안좋을 수 있다. 그래서 다음 카카오맵에선 타겟하나를 찝어 타겟내에 iframe을넣어 검색창을 띄우도록 embed()를 제공한다.

...
// 타겟으로 삼을 상태 정의
const [target, setTarget] = useState(null);

...

<div ref={setTarget} className={cx('address_modal')}></div>

위와 같은 방식은 리액트에서 ref의 변경사항을 수신하고 싶을때 적용해주는 방법.
주소정보에 관한 상태들을 useReducer로 관리.

function reducer(state, action) {
  return {
    ...state,
    [action.name]: action.value,
  };
}

...
 const [state, dispatch] = useReducer(reducer, {
    ...
    address: '',
    detailAddress: '',
    postNumber: '',
    state: '',
    city: '',
  });

온클릭시 주소검색창 함수

const openAddr = () => {
  // scroll 위치를 저장

  const currentScroll = Math.max(
    document.body.scrollTop,
    document.documentElement.scrollTop
  );
  new daum.Postcode({
    oncomplete: function(data) {
      let addr = ''; // 주소 변수
      let extraAddr = ''; // 참고항목 변수

      //사용자가 선택한 주소 타입에 따라 해당 주소 값을 가져온다.
      if (data.userSelectedType === 'R') {
        // 사용자가 도로명 주소를 선택했을 경우
        addr = data.roadAddress;
      } else {
        // 사용자가 지번 주소를 선택했을 경우(J)
        addr = data.jibunAddress;
      }

      if (data.userSelectedType === 'R') {
        if (data.bname !== '' && /[동|로|가]$/g.test(data.bname)) {
          extraAddr += data.bname;
        }
        // 건물명이 있고, 공동주택일 경우 추가한다.
        if (data.buildingName !== '' && data.apartment === 'Y') {
          extraAddr +=
            extraAddr !== '' ? ', ' + data.buildingName : data.buildingName;
        }
        // 표시할 참고항목이 있을 경우, 괄호까지 추가한 최종 문자열을 만든다.
        if (extraAddr !== '') {
          extraAddr = ' (' + extraAddr + ')';
        }

        dispatch({
          name: 'address',
          value: extraAddr,
        });
        dispatch({
          name: 'state',
          value: data.sido,
        });
        dispatch({
          name: 'city'
          value: extraAddr,
        });

        });
      } else {
        dispatch({
          name: 'address',
          value: 'extraAddr',
        });
        dispatch({
          name: 'state',
          value: data.sido,
        });
        dispatch({
          name: 'city',
          value: data.sigungu,
      target.style.display = 'none';

      // 우편번호 찾기 화면이 보이기 이전으로 scroll 위치를 되돌린다.
      document.body.scrollTop = currentScroll;

    },

    },
  }).embed(target);

  target.style.display = 'block';
};

처음에는 지번주소를 클릭했을때 상태값이 동기화 안되었는데 애초에 함수에서 분기해서 처리가 되길래 그에맞춰서 dispatch를 해주었다.

...
<div ref={setTarget} className={cx('address_modal')}>
                <div className={cx('modal_header')}>
                  <button
                    onClick={foldDaumPostcode}
                    className={cx('close_btn')}
                  >
                    x
                  </button>
                </div>
              </div>

아이프레임 모달이 켜지면 닫는 버튼이 없으므로. 커스타마이징 해서 닫는 버튼을넣어주며 클릭 시 모달을 끈다.

react-datepicker 달력 라이브러리에 한글 적용

흔히 널리 사용되는 달력 라이브러리들이 몇몇 있지만 기본적으로 한글이 적용되는 라이브러리는 거의 없을것이다. 그래서 조금 더 세팅을 해주어 달력에 한글을 입혀야한다.

import DatePicker, { registerLocale } from "react-datepicker";
...

const [Date, setDate] = useState(new Date());

return (
    <DatePicker
        locale={}
        onChange={setDate}
        minDate={new Date()} // 과거 날짜 disable
    />
)

달력에 좋고 유용한 기능들이 굉장히 많고 locale속성 registerLocale설정을 통해 달력에 한글을 전달할 수 있다. 하지만 라이브러리내에 한글정보가 없어서 전달할 수 없다.
하지만 date-fns에서 달력에 한글정보를 지원을 해준다. 그래서 해당 한글모듈을 불러와 적용할 수 있다.

import DatePicker, { registerLocale } from "react-datepicker";
import ko from 'date-fns/locale/ko';
registerLocale('ko', ko); // 등록

...

return (
    <DatePicker
        locale='ko'
        onChange={setDate}
        minDate={new Date()} // 과거 날짜 disable
    />
)

위와같이 적용해주면 한글이 적용된 달력을 확인할 수 있다.

인풋 첫글자 자동 완성 대문자

모바일 기기에서 해당 브라우저를 사용하며 다양한 변수가 생기는데, ios 사파리에서 접속해서 input에 영어를 작성하게되면, 첫글자가 자동으로 대문자로 들어갔다. 지웠다 쓰기를 반복해도 자꾸 대문자로 들어갔다. 그래서 짐작하건데 간단한 속성 하나 추가해주면 해결 될 것 같아 알아보았다.

autocapitalize

input 속성에는 autocapitalize라는 속성이 있는데, 해당 속성을 추가해주면 첫글자 대문자 자동완성을 하지 않는다.

...

const InputComponent = () => {

return (
    <input
    type="text"
    autocapitalize='off' />
)
}

위와 같이 autocapitalize속성을 추가하면 된다.

사파리 기기에서 첫글자가 자동으로 대문자가 된다면?

결제페이지를 작업하는중에 모바일 환경도 작업을 하는데, 사파리에서 키면 자꾸 영어의 첫 글자가 대문자로 자동 완성이 되었습니다.
따라서 불필요하게 자동완성이 된다면 해당 기능을 지워야 하는데 그에대한 처리는 input 의 속성으로 간단하게 처리할 수 있었습니다.

autoCapitalize

인풋의 autoCapitalize속성을 추가하여 위의 문제를 해결할 수 있습니다.

<input
               autoCapitalize="off"
               ...
             ></input>

이제 자동으로 대문자가 완성되지 않습니다.

input 필드의 읽기전용 속성

사용자 정보를 입력하는 input 필드를 작업하며 주소 정보가 있었는데, 주소 정보는 daum주소 api 검색을 통하여 정보를 입력하고 처리하였습니다. 근데 input의 value 입력은 일반적으로 입력하는게 아닌 검색한 값을 바탕으로 입력이 이루어져야 했는데, 따라서 input의 기본 입력자체를 막을 필요가 있었습니다. 그래서 input의 readOnly 속성을 활용 하였습니다.

// 주소 입력 input
<input
  autoCapitalize="off"
  value={state[checkTitle(el)]}
  onChange={(e) => {
  onChange={e => {
  onChange={(e) => {
    dispatch(e.target);
  }}
  readOnly
  name={checkTitle(el)}
  className={cx('input_form')}
  onClick={openAddr}
></input>

useState callback 사용하기

react class형 컴포넌트에서는 setState로 상태값을 바꾸면서 콜백을 전달해 상태값을 변경한뒤 해당 내용 실행이 가능합니다.

// 그냥 실행하면?
class State extends Component {
  state = 0

  componentDidMount() {
    this.setState(1)
    console.log(this.state) // 0
  }
}
// 콜백으로 실행하기
class State extends Component {
  state = 0

  componentDidMount() {
    this.setState(1, () => {
      console.log(this.state) // 1
    })
  }
}

종종 함수형 컴포넌트로 useState hook을 사용하면서 왜 클래스형 setState에 있는 기능이 함수형 setState에는 없을까 생각은 해보았지만, 해당 비동기 처리 문제를 안만나게끔 처리하면 되지 라는 생각으로 그냥 넘겼었는데, 이번에는 해당 기능을 함수형 컴포넌트에서도 사용해보기로 하였습니다.

useEffect 의존성 배열을 활용하기

useEffect를 사용하면 위와같은 효과를 낼 수 있습니다.

const [number, setNumber] = useState(0)

useEffect(() => {
  setNumber(1)
}, [])

useEffect(() => {
  console.log(number)
}, [number])

위와 같이 useEffect의 의존성 배열에 해당 상태를 넣어주면 해당 상태값이 바뀐뒤 실행하기에 마운트가 되고 number를 1로 바꾸고, console.log가 실행되어 1이 찍히게 됩니다.
근데 아직 부족합니다. 해당 console.log는 첫 마운트에도 찍히기 떄문입니다. 위의 문제를 해결하려면 첫 렌더링을 확인하는 ref변수를 관리해서 해결이 가능합니다.

const [number, setNumber] = useState(0)
const isFirstRender = useRef(true)

useEffect(() => {
  setNumber(1)
  isFirstRender.current = false
}, [])

useEffect(() => {
  if (!isFirstRender.current) {
    console.log(number)
  }
}, [number])

위 처럼 ref변수로 첫 마운트에 false로 바꿔주고 해당 변수가 false일때만 실행하면, 첫 렌더링에는 console.log를 하지 않습니다.
https://stackoverflow.com/questions/56247433/how-to-use-setstate-callback-on-react-hooks

리액트, 타입스크립트 name of undefined 에러

리액트 타입스크립트 개발을 진행하다 갑자기 아래와 같은 에러가 생겼습니다.

line: 0 parse error cannot read property name of undefined

name이라는 프로퍼티를 사용하지 않았는데 왜 undefined의 name에 접근했다는거지??? 라는 의문을 가지다가, 해당 에러관련해서 찾아보았는데 찾은 이유는 리액트스크립트 버전에 비해 타입스크립트 버전이 낮아서 생기는 오류라고 합니다. package.json을 확인해보니 타입스크립트 버전이 밀려있었습니다. 따라서

npm uninstall typescript
npm install typescript

패키지를 지운 뒤, 다시 인스톨 해줘서 버전을 맞춰주니 해당 에러가 사라졌습니다.

withRouter를 통해 history 객체를 받아올때 타입 매칭

타입스크립트에서 history객체를 받아올때 타입 매칭이 잘 안되어 any로 작업을 했었는데, 타입 지정을 해주기 위해 매칭방법을 알아보았습니다. 방법은 간단했는데, ‘react-router-dom’에서 RouteComponentProps 라는 타입을 제공해주었습니다. 해당타입을 활용하여 history매칭이 가능합니다.

import { RouteComponentProps } from 'react-router-dom';
// 일단 react-router-dom 에서 해당 interface를 불러온다

...

interface childComponentProps extends RouteComponentProps<any> {
  // 나머지 props들
}
//인터페이스를 정의하며 위에서 불러온 RouteComponentsProps 를 extends 한다.

const getHistory: React.SFC<childComponentProps> = ({ history, ...otherProps }) => {
...
return {
    ...
}
}
// history를 받아올 컴포넌트에 위에서 정의한 interface를 적용시킨다.
export default withRouter(getHistory);

위와같이 정의해주면 history객체에 대한 타입매칭을 해줄 수 있습니다. RouteComponentProps인터페이스가 어떻게 정의되었는지 node_modules에서 확인해보면

export interface RouteComponentProps<
  Params extends { [K in keyof Params]?: string } = {},
  C extends StaticContext = StaticContext,
  S = H.LocationState
> {
  history: H.History<S>
  location: H.Location<S>
  match: match<Params>
  staticContext?: C
}

위와 같은 형태를 가지고 있습니다. 그렇군요.. history,location, match 등 withRouter로 감싸줄때 받는 정보를 가지고 있습니다.

브라우저 새탭에서 열기, 새창에서 열기

javascript를 사용해서 브라우저 새탭으로 열기, 새창으로 열기를 적용할일이 생겨서 알아보게 되었습니다. 생각처럼 간단하게 적용이 가능했습니다.

window.open('link')

위처럼 window.open()함수를 사용하면 함수 실행 시 새창에서 입력한 link로 브라우저가 열리게됩니다. 그렇다면 새탭에서 열려면?

window.open('link', '_blank')

두 번째 파라미터에 ’_blank’를 입력해주면 새창이 아닌, 새탭에서 입력한 link로 브라우저가 오픈됩니다.

모바일 사파리에서 키보드가 나올때 포지션 깨짐현상

아이폰에서 접속 시 input을 클릭했을떄, 키보드가 나오는데 그 키보드가 올라오면서 input에 포커스가 되는게 아니라, 스크롤을 내리고 input을 클릭할때 스크롤이 내려가있는 문제가 생겼습니다. 해결해나가며 이것 저것 많이 시도해보았는데, 삽질을 많이하여 정리해보려고 합니다.
일단 처음에 든 생각. 스크롤이 내려간다면 그냥 포커스가 되었을때 스크롤을 input이 있는곳으로 올려주면 되지 않을까? 라는 생각을 했고, 실제로 스택오버플로우에도 비슷한 내용이 많았습니다.

const inputFocus = () => {
    const [target, setTarget] = useState(null);

    return <input ref={setTarget}  ... />
}

일단 input 요소를 ref로 잡아줍니다. 그리고 스크롤을 올려주는 솔루션들은 다양하게 있었는데 하나씩 적용해보았습니다.

{
  target.current.scrollIntoView({
    behavior: 'smooth',
    block: 'start',
  })
}

scrollIntoView를 적용해보았는데, 작동하지 않았습니다 이유는 모르겠네요… deprecated되었다는 말도 있고… 정확한건 찾지 못했습니다 일단 오작동

 {
    inputRef.current.onfocus = () => {
      window.scrollTo(0, 0);
      document.body.scrollTop = 0;
    };

onfocus라는게 있었네요. 포커스가 될 때 해당 함수를 실행합니다. 콘솔로그를 찍어보면 잘 찍히는데, 스크롤은 그대롭니다. 콘솔이 찍히는걸보면 스크롤자체가 안되는건가 싶기도 했고, 잘 모르겠습니다.
인풋 폼 클릭시 핸들링하던 state값이 하나가 있었는데 그 값을 활용해보기로 했습니다.

useEffect(() => {
  window.scrollTo(0, 0)
  document.body.scrollTop = 0
}, [changedState])

네. 안됩니다. 해당 상태값이 바뀔때 스크롤을 최상단으로 올려도 작동하지 않습니다. 이쯤되서 깨달음을 얻은것이 하나가 있는데, 화면 상단에 위치한 input폼은 position fixed로 고정시켜놓았는데, 어떻게 밀려날 수 있지? 라는 생각을하다가 해당 내용을 알아보니 모바일 사파리에서 키보드가 올라오면 position fixed는 적용이 안된다고 합니다. 그래서 키보드가 올라올때 fixed를 absolute로 바꿔보고 했지만. 역시 되지 않습니다. 이쯤되면 기능들을 추가할때 콘솔도 잘 찍히겠다.. 윈도우의 스크롤이 문제인가? 스크롤 라이브러리를 적용해서 스크롤을 올려보기로 했습니다.

import { animateScroll } from 'react-scroll';

...
<div>
onClick={() => {
          animateScroll.scrollToTop({ smooth: false, delay: 0 });

        }}
</div>

해당 라이브러리를 사용하니 스크롤이 정상적으로 input이 포커싱 될 때 올라갑니다. 역시 스크롤이 문제였습니다.. 항상 데스크탑웹이랑 모바일에서의 웹은 다르게 작동해서 어려운 부분이 많은데, 이번 버그도 정확히 원인을 파악하거나 하지는 못했지만 확실히 기억해둬야 겠습니다.

웹팩에서 public폴더 사용하기

CRA프로젝트를 진행할때에는 json 목데이터나 static한 파일들을 public폴더에 넣어두고, 프로젝트 내에서 해당 public폴더에 접근이 가능해서 유용하게 사용했던 기억이 있습니다. 그래서 이번 작업에서도 public폴더를 사용해보려고 하는데, 접근이 안되었습니다. 웹팩으로 셋팅이 되어있는데, CRA에서 public폴더를 사용할 수 있게 해주는구나 생각이 들었는데.. 그럼 어떻게 웹팩에서 해당 내용을 적용할까 알아보앗습니다.

copy-webpack-plugin

웹팩에 copy-webpack-plugin이 있는데 위 플러그인은 이미 존재하는 개인적인 파일이나 디렉토리를 빌드 디렉토리에 복제한다. 라고 소개하고있습니다. 복제를 하면 될까? 일단 설치해봅시다.

npm install copy-webpack-plugin --save-dev

문서를 확인해보니 사용법이 나와있습니다.

const CopyPlugin = require('copy-webpack-plugin');

module.exports = {
  plugins: [
    new CopyPlugin({
      patterns: [
        { from: 'source', to: 'dest' },
        { from: 'other', to: 'public' },
      ],
    }),
  ],
};

webpack.config.js에서 위와같이 사용하라네요 프로젝트에 적용해봅니다. 처음에 stackoverflow에서 먼저 내용을 보았는데 스택오버플로우에 나와있는 방법들은 조금 오래되서 현버전에 적용이 안되는듯합니다. 객체안에 patterns의 키에 옵션을 넣어주어야 합니다.

const CopyWebpackPlugin = require('copy-webpack-plugin'); // 불러오기
...
module.exports = {
  ...
  plugins: [
    ...
    new CopyWebpackPlugin({ patterns: [{ from: 'public' }] }),
  ]
}

옵션의 필수값은 from 하나 인 것 같습니다. to어디로 복제할지 옵션은 기본값이 웹팩 output의 경로 라고 나와있습니다. 이제 public디렉토리에 mock json 데이터를 만들어봅니다.
/public/data/mock.json

{
  "test": "테스트를 해보자"
}

데이터를 만들고 해당 json 데이트럴 불러옵니다.

...

useEffect(() => {
const getData = async () => {
      console.log('ho');
      const { data } = await axios.get(
        'http://localhost:8080/dist/data/mock.json',
      );
      console.log(data);
    };
    getData();
},[])
...

콘솔을 찍어보면 json 데이터가 정상적으로 잘 들어오고 있는것이 확인이 됩니다. webpack output의 값이 dist이기에 dist경로로 들어가있는것을 확인할 수 있습니다.

리액트 다중 요소 ref값 붙인 뒤 확인하기

ref는 항상 다루기 까다롭고 톡톡 튀는 느낌입니다. 항상 고생을 많이 하는데 useRef를 사용하여 여러 요소에 ref를 다는 방법은 이전에도 정리를 했지만, 다음과 같습니다.

const Ref = () =>{
const [list, setList] = useState([1,2,3,4,5])
const refs = useRef([]);

return list.map((el, index) =. (
  <li
  ref={el => refs.current[index] = el}
  />
))
}

만약 해당ref들이 붙은 시점에서 무언가를 실행해야 한다면??

const Ref = () =>{
const [list, setList] = useState([1,2,3,4,5])
const refs = useRef([]);

useEffect(() => {
  console.log(refs)
},[refs])

return list.map((el, index) =. (
  <li
  ref={el => refs.current[index] = el}
  />
))
}

useEffect의 의존성 배열에refs를 넣어주고 console.log를 해보면, 바뀐 값이 확인이 되지 않습니다. 생각 해보면 refs객체의 current key에 dom요소들이 담기는데, refs객체는 변하지 않기 떄문

const Ref = () =>{
const [list, setList] = useState([1,2,3,4,5])
const refs = useRef([]);

useEffect(() => {
  console.log(refs)
},[refs.current])

return list.map((el, index) =. (
  <li
  ref={el => refs.current[index] = el}
  />
))
}

current값 까지 넣어주게 되면?? 응 안돼~ 라고 합니다. current값이 변하는 값이며 변하기는 하지만, useEffect가 발생하는 시점이 레이아웃이 모두 그려지고, ref값이 요소에 붙었을거라는 보장은 못하는듯 합니다. ref가 붙을때는 요소들의 레이아웃이 모두 그려진 뒤, 그 후 약간의 텀이 지난 뒤 붙는것 같습니다. 가끔 콘솔을 보면 current값이 null이라고 나오는데, 클릭해보면 요소들이 붙어는 있고.. 붙는 과정인것같습니다. ref객체는 우리에게 ref값의 변화를 알려주지 않습니다.

위의 해결방안으로 state를 활용하는 방법인데,

const Ref = () =>{
const [list, setList] = useState([1,2,3,4,5])
const [target, setTarget] = useState(null)

useEffect(() => {
  console.log(refs)
},[target])

return list.map((el, index) =. (
  <li
  ref={setTarget}
  />
))
}

setState함수를 콜백 ref로 전달해주면, target값의 변화를 잡을 수 있습니다. 여기서 그렇다면 여러 요소들을 위의 방법으로 담을 수 있을까가 문제인데, 해당 콜백ref를 전달하는 부분에서 setTarget의 인자로 배열을 활용해 요소들의 ref값들을 넣어주어도, 계속 추가가되거나 하나만 추가되거나 하는 문제들이 있었습니다. 그래서 일단 잡은 솔루션은 변화를 관찰하는 상태값 하나를 만든 뒤,

const Ref = () =>{
const [list, setList] = useState([1,2,3,4,5])
const refs = useRef([]);
const [test, setTest] = useState(null)

useEffect(() => {
  console.log(refs)
},[refs.current])

return list.map((el, index) =. (
  <li
  ref={(el) => {
    refs.current[index] = el;
    if (index === list.length - 1) {
      setTest('ho');
    }
  }}
  />
))
}

요소의 마지막에서 해당 임시 상태값을 바꿔주면 캐치가 가능해집니다. 가 상태를 하나 만들고 사용해야한다는게, 확실하며 효울적인 접근법은 아닌느낌이 강하지만, 일단 현재까지 찾은 솔루션은 위의 방법으로 사용하는 것 입니다.

create-react-app 빌드와 사용자정의 경로

cra에서 빌드 시 모듈 파일들이 바라보는 경로를 설정하려면? 일단 기본적으로 cra로 프로젝트를 만들고, npm run start를 하여 프로젝트를 실행하면 body의 스크립트 태그에

<script src="/static/js/name">

위와 같이 절대 경로로 파일이 불러와 집니다. 하지만 커스텀경로로 불러오고 싶다면? eject를 하여 webpack을 설정하는건 까다로운 작업이 될 것 같고… 간단한 방법이 있나 알아보았습니다.

homepage

package.json에 homepage라는 내용을 넣어서 빌드 후 경로설정을 할 수 있습니다.

// package.json
{
  "homepage": "./"
}

위와같이 homepage를 설정해준뒤, 경로를 확인해보면

<script src="./static/js/name">

경로가 바뀐 모습을 확인할 수 있습니다.
원래 빌드 파일이 상대 경로로 지정되어있어서 모듈을 불러오지 못하는 문제 때문에 알아보게 되었는데, 해당 경로를 절대경로로 수정한 뒤 문제를 해결하며 알아보게 되었습니다.


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

GitHub