배운내용 열심히 정리하기

객체와 스토리지 활용, 배열에서 요소 찾기, 모달에서 배경 스크롤 잡기, ref와 옵저버, 모바일 input텍스트입력기 입력 후 안보이게 하기, includes()메소드, .env파일 환경변수 접근, 큰사이즈의 이미지 불러오는 시간 벌기, 웹팩과 함께 codespliting적용, reduce를 사용한 컴포넌트 리팩토링, 라우트 이동 시 기존 ajax요청 취소 처리, 웹팩 감시모드에서 청크파일들이 cleanup이 안되는 상황 처리,

객체와 스토리지 활용

스토리지에 자바스크립트 객체 형태의 내용을 저장하면 형태가 온전히 유지되지 않고, 문자열 형태로 저장이되어 다시 꺼내서 사용할때 원하는 형태로 사용이 안된다. 예를들어서

const obj = {
  a: 1,
  b: 2,
  c: 3
};

localStorage.setItem('obj',obj);
...
localStorage.getItem('obj')
// {"a":1, "b": 2, "c":3}

위와같이 단순 문자열 형태를 띄게 된다. 위와같은 문제를 해결하기위해 자바스크립트에서 JSON.parse 메소드를 제공한다. 객체형태가 스토리지에 저장되었을때 문자열화 되어 사용못하는것을 다시 객체형태로 잡아준다.

const obj = {
  a: 1,
  b: 2,
  c: 3
};

localStorage.setItem('obj',obj);
...

JSON.parse(localStorage.getItem('obj'))
// { a: 1, b: 2, c:3}
JSON.parse(localStorage.getItem('obj')).a
// 1

객체화 시켜서 온전한 형태로 사용할 수 있다.

배열에서 요소 찾기

원래 배열에서 요소를 찾으려면 전에 정리했던 요소 in 배열 문법을 사용하려고 했는데, in문법을 잘못 이해하고있었다. 배열에서 요소를 찾는 문법이 아니었다. 그래서 다른 방법을 찾아보다가 javascript find 문법을 알아보았다.

arr.find(callback, thisArg)

find메서드는 두 개의 인자를 받으며 콜백함수는 세 인자를 받는데, el요소, index, array find함수를 호출한 배열을 받고, thisArg는 콜백이 호출될 떄 this로 사용할 객체이다. 메서드 이름 그대로 find해준다.

const arr = [1, 2, 3, 4, 5]

const result = arr.find(el => el > 3)

console.log(result)
//4

결과는 4를 반환하는데 5도 3보다 큰데 4만 반환하는 이유는 find메서드는 주어진 판별 함수를 만족하는 첫 번쨰 요소의 값을 반환한다. 그런 요소가 없다면 Undefined를 반환한다.
!! 배열 요소가 해당 배열에 존재하는지 확인하고자 한다면 array.indexOf() 또는 array.includes()를 활용할것!
그래서 나는 해당 배열을 가져와서 find메소드에서 해당 결과값이 참인지 거짓인지 판단해 활용하였다.

모달에서 배경 스크롤 잡기

모달을 구현하는데 화면 가운데에 모달이 생기고 배경으로는 dim을 넣어주었다. 모달자체가 내용이 길어서 스크롤이 되었는데 모달의 스크롤이 다 되고나면, 모달뒤의 원래 배경이 스크롤이 되는 현상이 있어서 그것을 막기위해 여러 방버을 찾아보았다. 모달 on 시, 단순히 body의 overflow를 오토로 맞춰주고 스크롤을 막는 방법이 있었는데 그렇게하면 모달이 꺼졌을떄 스크롤이 최상단으로 올라가는 문제가 생겼고, 여러가지 솔루션들이 있었지만 다 적용해도 문제가 생겼다. 그러다가 찾은 방법이다.

// 모달 컴포넌트에서
useEffect(() => {
  document.body.style.cssText = `position: fixed; top: -${window.scrollY}px`
  return () => {
    const scrollY = document.body.style.top
    document.body.style.cssText = `position: ""; top: "";`
    window.scrollTo(0, parseInt(scrollY || '0') * -1)
  }
}, [])

위와같은 내용을 정의해주면 모달이 켜질때 해당 함수가 실행되는데 뒤의 배경이 스크롤이 되지도 않을뿐더러 모달이 꺼졌을때에도 원래 스크롤 포지션을 유지해준다. 완벽한 솔루션이였다.
참고: https://medium.com/@bestseob93/%ED%9A%A8%EC%9C%A8%EC%A0%81%EC%9D%B8-%EB%A6%AC%EC%95%A1%ED%8A%B8-%EB%AA%A8%EB%8B%AC-react-modal-%EB%A7%8C%EB%93%A4%EA%B8%B0-bd003458e9d

ref와 옵저버

intersection observer를 요소에 등록하기 위해 해당 요소의 DOM에 접근해야했고 ref를 사용해야 했다. 다중 ref를 만들어줘야했고 전에 정리한 내용대로 정리했찌만 적용이 잘 안되었다.

const [state, setState] = useState([])

useEffect(
... api 요청
)

const refs = useRef(state.map(createRef))

useEffect(
... refs 관찰자 등록
, [refs])

이런식으로 구현을 했는데 데이터 요청 전 refs가 비어있기에 데이터 요청 후 다시 읽어도 refs가 채워지지 않았다. (100%는 아니고 거의 확실한 내 추측) 간단하게 생각해서 그렇다면 조건을 걸어서 조건을 주고 데이터가 있을때만 해당 코드를 실행시키려 하였다.

const [state, setState] = useState(null)

useEffect(
... api 요청
)


if (state)
const refs = useRef(state.map(createRef))

useEffect(
... refs 관찰자 등록
, [refs])

안되더라 훅을 사용할땐 최상단에서 사용하라 뭐 이런 내용의 에러가 나오고 조건안에서 사용하지 말라고 한다. 그외 비슷한 이것저것 방법을 시도했다. 똑같은 화면만 몇시간동안 보면서 계속 시도해보았지만 다 안되었다 너무 스트레스 받고 힘들었따 하루종일 문제해결을 못했다. 이해는 둘쨰치고 구현자체가 안되니 머리아팠다. 그냥 이것저것 다 쑤셔넣어보자 마음먹고 그러기 시작했다.

// const refs: any = React.useRef((list as any[]).map(React.createRef));
const refs: any = useRef([]);
// const refs: any = [];

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

useEffect(() => {
  console.log(list);
//  refs.current = refs.current.slice(0, list.length);
  registerObserver(refs);
}, [list]);


return (
...
          {(list as any[]).slice(0, 10).map((el, index) => {
            return (
              <li
                // ref={(els) => (refs[index] = els)}
                ref={(el) => (refs.current[index] = el)}
                /*

확실히 기억에 남는 하나는 요즘엔 16.3 이후부터 createRef()를 해서 변수에 담고 그 변수를 요소에 지정해서 레프를 달고 이전에는 ref를 만들어서 콜백으로 세부적으로 레프를 담아주었던것. 이건 기억에 남는다.
아직 구현이 다 된건 아니지만 주석 되어있는 부분중 refs = [] 빈배열로 해놓고 채워넣는 방식은 작동은 되나 관찰자 함수가 자꾸 2번이나 실행이 되었고 위와같이 하니 일단은 정상작동 하는것처럼 보인다. 왜 이거는 되고 전 방식은 안되는지 아직 모르겠다 많이 부족하다.

모바일에서 input 텍스트입력기 입력 후 안보이게 하기

생각하지 못했던 부분. 모바일 에서는 키보드가 따로 없기에 input요소에서 텍스트 입력기가 나온다. 입력을 하고나서 자연스레 입력기가 사라지지 않았다. 그에 방법을 찾아보았다.

elementRef.current.remove()

그러다가 요소 remove를 찾게되었다. 적용 예시

...
const inputRef = useRef(null)
...
  <form
    onSubmit={(e) => {
        e.preventDefault();
        ... 제출함수
        inputRef.current.remove();
      }
    }}
  >
  ...
      <input
        type="search"
        ref={(el) => (inputRef.current = el)}
        onChange={(e) => {
          setValue(e.target.value);
        }}
      />

      ...

인풋 요소에 ref를 달아주고 form 제출 시 해당 요소를 ref.current.remove() 해주니 입력기가 사라졌다. 근데 요소 자체가 사라지기때문에 입력기가 사라진것… 요소는 유지하며 입력기만 안보이게 해야했다. remove는 요소를 없앨 상황에 활용하면 될것같다.

elementRef.current.blur()

그러다가 다른 방안을 찾아보는데 blur라는 기능이 있더라. 이런 모바일 기능은 웹 개발 작업에서 확인할 수 없어서 까다로웠는데 적용해보기로 하였다.

...
const inputRef = useRef(null)
...
  <form
    onSubmit={(e) => {
        e.preventDefault();
        ... 제출함수
        inputRef.current.blur();
      }
    }}
  >
  ...
      <input
        type="search"
        ref={(el) => (inputRef.current = el)}
        onChange={(e) => {
          setValue(e.target.value);
        }}
      />

      ...

요소에 blur를 적용하니 제출 후 입력기가 사라졌다. 잘 활용할 수 있을것 같다.

includes() 메소드

유용한 메소드. includes메소드는 한마디로 배열이 특정 요소를 포함하고 있는지 판별한다. 사용법은 다음과 같다.

const arr = [1, 2, 3, 4, 5]

arr.includes(1) // true

arr.includes(2) // true

arr.includes(6) // false

위처럼 배열에 해당 요소가 있는지 없는지 확인한 후 boolean값을 반환한다. 배열 안에 숫자요소 뿐 아니라 문자열 요소도 찾아준다.

const arr = ['ho', 'hi', 'kiki']

arr.includes('ho') // true

arr.includes('kiki') // true

arr.includes('ayo') // false

여기서 생기는 궁금증 유사배열 요소에서는 ???

const stringArr = 'abc'

stringArr.includes('a') // true

stringArr.includes('b') // true

stringArr.includes('') // true

stringArr.includes('f') // false

똑같이 작동하며 ”에도 true값을 반환하는것을 확인할 수 있다. includes함수를 적재적소에 잘 활용하면 좋을 것 같다.

.env파일 환경변수 접근

보통 프로젝트에서 .env파일에 환경별로 셋팅값에 따라 값을 활용하는데 이번에 그 값을 가져와 적용해 보았다.
처음에는 어떻게 접근해야하는지 난해하여 알아보았는데 찾아보니 많이 보던 경로로 접근을 한다. env파일에 접근을 할때에는 process.env 객체를 활용하여 해당 변수에 접근을한다.

//.env
MY_VAR = 1234
YOUR_VAR = 5678

...

// .js
console.log(process.env) // {}

처음에 process.env파일에 콘솔을 찍어보니 그냥 빈객체만 나와서 당황스러웠다. 이부분은 webpack설정에 플러그인을 통하여 보이게 할 수 있다고 한다.

//.env
MY_VAR = 1234
YOUR_VAR = 5678

...

// .js
console.log(process.env.MY_VAR) // 1234
console.log(process.env.YOUR_VAR) // 5678

env자체는 빈객체로 보일지라도 해당 변수에 접근이 가능하다. 대소문자를 가리니 올바르게 작성해서 접근해야한다.

큰사이즈의 이미지 불러오는 시간벌기

이미지 사이즈가 큰 것들은 브라우저가 이미지를 불러오는데 상대적으로 시간이 더 든다. 따라서 불러오는동안 레이아웃에 영향을 미쳐 사용자경험 측면에서 해를 끼칠 수 있는데, 처리 해나가는 과정을 기록한다.
이미지의 데이터를 받아서 렌더링 할 때 첫 시도한 방법은 로딩상태를 관리하여 로딩 시간을 적용하는 방법이었다.

...
const [list, setList] = useState([])
const [loading, setLoading] = useState(false)

useEffect(() => {

const setData = async () => {

setLoadnig(true)

try {
...
} catch(e) {
...
}

setLoadnig(false)

}

},[])

여기에서 별 효과가 없는걸 보고 깨달았다. 데이터 불러오는것은 빠르나 이미지를 그리는데 시간을 소요한다. 그렇다면 그 시간을 어떻게 벌며 처리할것인가가 문제인데, img태그의 onload기능을 사용하기로했다. onload는 이미지가 그려지고 나서 실행되는 함수.

const [list, setList] = useState([])
const [firstLoading, setFirstLoading] = useState(false)

...
return {
...
<img src={img} onload={() => setFirstLoading(true)} />
...
}

일단 첫 이미지가 그려지고나서 완료되었다는 신호는 받는다. 그 후에 약간 눈속임 작업이 필요하다.

...

return(
{!firstMount && <div style={이미지와 같은 사이즈의 빈 div} />}
<img src={img} onload={() => setFirstLoading(true)} />
)

이미지와 같은 사이즈의 div를 두어 일단 공간을 차지하게하고 로드되고나면 없애준다.
그러나 아직 온전해보이지 않는다. 가끔 속도가 느릴 때 빈 디브와 로드가 살짝 덜된 이미지가 동시에 공간을 잡아먹을떄가 있따.

...

return(
{!firstMount && <div style={이미지와 같은 사이즈의 빈 div} />}
<img src={img} onload={() => setFirstLoading(true)}
style={{
display : `${firstMount} ? '' : none`
}}
/>
)

로드가 되기 전까지 display none으로 공간처리를 해준다.

웹펙과 함께 code splitting 적용

code splitting은 프로젝트 규모가 커질수록 거대해지는 bundle.js 파일을 나눠서 번들링하여 필요할때 불러오는 작업이다. dynamic import와 loadable/component 라이브러리를 적용하면 쉽게 적용할 수 있을 거라 생각했지만 문제가 많았다.

const split = lodable(() => import('../pages/Search/Search'))

const Routes = () => {
  return (
    <Router>
      <Switch>
        <Route exact path="/" component={Main} />
        <Route exact path="/split" component={split} />
      </Switch>
    </Router>
  )
}

export default Routes

라우트 파일에 split 컴포넌트를 loadable을 사용하여 동적으로 가져온다. 그랬더니 split에 갔을때, chunk파일을 불러오지 못한다는 에러가 나왔다. 그 후 웹팩작업.

output: {
  filename: 'js/[name].bundle.js',
  path: path.resolve(__dirname, 'dist'),
  publicPath: '/app/dist/',
  chunkFilename: 'js/[name].[hash].bundle.js',
},

아웃풋에 chunkFilename 내용을 추가한다.
그 후 바벨이 읽어도 무시하게끔 처리.

// .babelrc
{
  "presets": ["@babel/preset-env", "@babel/preset-react"],
  "plugins": ["syntax-dynamic-import"]
}

syntax-dynamic-import 를 설치하고 플러그인을 적용해준다. 아직도 에러가 생긴다. 스택오버플로우를 많이 뒤져보았지만, 다들 비슷한 솔루션이다. 왜 안될까 이런저런 접근을 해보다가 바벨쪽에 내용을 추가해보았따.

{
         test: /\.(tsx|ts|js)?$/,
         exclude: [/node_modules/],
         // use: ['babel-loader'],
         use: [
           {
             loader: 'babel-loader',
             options: {
               plugins: ['@babel/plugin-syntax-dynamic-import'],
             },
           },
         ],
       },

webpack.config.js 에서 바벨로더에 옵션을 추가하고 babelrc에 추가했던 플러그인을 추가해주었다. 그랬더니. 페이지 라우팅 시 해당 번들링js파일을 잘 가져왔다. 그래서 이렇게 해결했다고 생각했는데, 해당 부분을 지우고 나서도 원활히 돌아간다. 왜 안되었고 왜 되는지 모르겠다. 원인 파악을 못했다. 참 어려운것 같다. 다음에 다시 확인해봐야겠다.

reduce를 사용하여 컴포넌트 리팩토링 작업

props로 Line수 에 해당하는 데이터를 전달받아 그 line 수에 맞춰 렌더링하는 컴포넌트를 구현해야했다.
일단 아이템을 리턴하는 함수 정의

const itemReturnFunc = (list) => {
  return list.map((el, index) => (
  ...each item
  )

배열을 받아서 순회하며 리턴해주는 함수이다.

<Container>
  <div className={cx('wrapper')}>{itemReturnFunc(list.slice(0, 10))}</div>
  {props.rows > 1 &&
    secondList.reduce((acc, current, idx) => {
      if (!((idx + 1) % 10)) {
        return acc.concat(
          <div key={idx} className={cx('other_wrapper')}>
            {itemReturnFunc(secondList.slice(idx - 9, idx + 1))}
          </div>
        )
      }
      return acc
    }, [])}
</Container>

일단 2번쨰 줄부터 다른 컨테이너를 사용해야 해서 위와같이 적용 해야했는데, 리스트 상태를 두 개로 나눠서 처리하였다. 첫번째 줄 데이터리스트, 그 후의 리스트들 데이터리스트 로 처리하고 전달받은 row가 더 있을때 reduce로 해당 배열을 반환하여 처리를 해주었으며, 리스트가 10개씩 들어가야 했기에 10개보다 모자라면 짤랐다. list를 props로 내려주거나 할때는 slice를 사용할 경우 React의 얕은비교가 잘 이루어지지 않아 컴포넌트 최적화시 문제를 야기할 수 있으니 주의해야한다.

라우트 이동 시 기존 ajax 요청 취소 처리

페이지가 1,2,3 있다면 1에 접속했을 때 마운트 되었을때 처리하는 ajax요청이 있다고 가정한다면 1에 접속하고 바로 2에 접속해버리면 요청이 끝나지도 않았는데 언마운트가 되고, 2로 넘어간다. 그러면 리액트에서는 언마운트 되었는데 상태값을 바꾸려 하지 말아라 메모리 누수가 생긴다 라는 에러를 뱉는다 (데이터와 상태를 바인딩 한다면) 그래서 언마운트 되었을때 취소를 어떻게 할 수 있을까 알아보았다. 일단 해결 자체는 요청 취소가 아닌 언마운트 되었으면 상태값을 세팅하지 않으면 되는것이다.

...
const [list ,setList] = useState([])
const isMounted = useRef(true) // ref로 변수관리

const setList = useCallback(async () => {
  try {
    const data = await axios.get(``);

    if (isMounted.current) { // 여기에 상태 를 바꿀 때 조건을 넣어준다
      setList(data);
    }
  } catch (e) {
    console.log(e);
  }
}, []);
...

그 후 뒷정리 함수에서 언마운트 처리

useEffect(() => {
  setList()

  return () => {
    isMounted.current = false
  }
}, [])

뒷정리 함수에서 언마운트가 되면 isMounted의 값을 false로 바꾼다. 그러면 상태값 바꿀 때 걸린 조건에 걸려서 언마운트 시 상태를 바꾸지 않는다. 이 외에도 커스텀hook을 만들어 관리한다거나 다른 방법을이 꽤 있는것 같다.
참고 : https://stackoverflow.com/questions/49906437/how-to-cancel-a-fetch-on-componentwillunmount

웹팩 감시모드에서 청크파일들이 cleanup이 안되는 상황 처리

웹팩에서 cleanupplugin을 사용하면 사용하지 않은 빌드된 파일들을 알아서 정리해준다. 근데 개발을 하다가 dist폴더를 보니 chunk 파일들이 쭉 쌓여있었다. 이유를 알아보니 빌드 이전 이나 전체 웹팩 처리 후 작동해서 처리가 안된다고 한다. 그래서 on-build-webpack이라는 플러그인을 사용해보기로 했다. 해당 플러그인은 빌드가 완료되면 콜백을 전달하여 실행할 수 있다. 그리고 부가적으로 fs도 받아준다.

npm i fs
npm i on-build-webpack

빌드 후 어떻게 실행할거냐? 이것이 문제인데 다음과 같이 적용해주면 된다.

const WebpackOnBuildPlugin = require('on-build-webpack');
const buldDir = './dist';
const fs = require('fs');


... plugin [
new WebpackOnBuildPlugin(function (stats) {
        const newlyCreatedAssets = stats.compilation.assets;

        const unlinked = [];
        fs.readdir(path.resolve(buildDir), (err, files) => {
          files.forEach((file) => {
            if (!newlyCreatedAssets[file]) {
              fs.unlink(path.resolve(buildDir + file));
              unlinked.push(file);
            }
          });
          if (unlinked.length > 0) {
            console.log('Removed old assets: ', unlinked);
          }
        });
      }),
      ]
      ...

이러면 빌드 후 이전것들과 비교해 사용되지 않는 이전chunk 파일들을 제거해준다.


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

GitHub