728x90
// < 18
type TagPrpos = {
  name: string,
};

const Tag: React.FC<TagProps> = ({ name }) => (
  <div>
    <div>{ name }</div>
    { children }
  </div>
);
// > 18
type TagPrpos = {
  name: string,
  children: react.ReactNode
};

const Tag: React.FC<TagProps> = ({ name, children }) => (
  <div>
    <div>{ name }</div>
    { children }
  </div>
);

 

children 요소를 명시적으로 선언해줘야한다.

< 18에서는 children이 optional로 선언되어있었던 반면,

18에서는 optional 선언도 제거되었기 때문이다.

728x90
반응형
728x90

문제 상황

// _app.tsx

function App({ Component, pageProps }) {
  useEffect(() => {
    console.log('rendering')
  }, [])
  
  return <Component {...pageProps} />
}

영향을 줄만한 코드 일체없이

_app.tsx에서 위와 같이 코드를 설정했음에도

useEffect가 두번도는 큰 문제가 발생했다.

 

이것을 나중에 api요청과 처리 과정에서 발견했는데,

redux 코드에 문제가 있는건지

re-rendering 과정에 사이드 이펙트가 발생하는건지

한참을 돌고 돌아 문제의 원인을 찾았다.

(15분은 낭비한듯..)

 

문제 원인

바로, react의 strict모드 설정 이슈였다.

strict모드의 어떤 부분에서 이슈가 발생했는지

stict모드가 무엇인지 살펴보자.

 

react docs에 보면 아래와 같은 문장이 있다.

Strict mode can’t automatically detect side effects for you, 
but it can help you spot them by making them a little more deterministic. 
This is done by intentionally double-invoking the following functions:

1. Class component constructor, render, and shouldComponentUpdate methods
2. Class component static getDerivedStateFromProps method
3. Function component bodies
4. State updater functions (the first argument to setState)
5. Functions passed to useState, useMemo, or useReducer

사이드 이펙트를 방지하는데 도움을 주기위해

5가지 경우에 대해 함수를 두번 호출한다고 명시되어있다.

 

그 중에서도 내 눈에 띈것은 Function component body...

함수형 컴포넌트의 Body구문에서 두번 호출된다면

어지간하면 두번 호출된다는 것이라 생각된다.

 

문제 해결

그렇다. strict 모드를 해제하면 일단

두번 호출되는 문제는 해결된다.

// next.js 기준
// next.config.js

/** @type {import('next').NextConfig} */
const nextConfig = {
    reactStrictMode: false, // <-- 이 부분 false로 변경
    swcMinify: true
};

module.exports = nextConfig;

 

Strict 모드란?

strict모드가 어떤 도움을 주는지

해제했을때 어떤 문제가 발생할 수 있는지

가볍게 살펴보자

 

문서 첫 줄에 이렇게 명시되어있다.

"StrictMode는 애플리케이션 내 잠재적인 문제를 알아내기 위한 도구이다."

그리고 "Strict 모드는 개발 모드에서만 활성화되며, 프로덕션에 영향을 주지는 않는다."

 

즉 위의 문제도 사실 프로덕션에서는 발생하지 않을 이슈였던것이다.

그럼에도 개발단에 거슬리긴 했다.

 

다른 도움되는 부분이 없다면 해제하고 마무리하는것으로 하자.

Strict가 도움을 주는 부분 6가지 (확장 예정)

1. Identifying components with unsafe lifecycles
2. Warning about legacy string ref API usage
3. Warning about deprecated findDOMNode usage
4. Detecting unexpected side effects
5. Detecting legacy context API
6. Ensuring reusable state

 

사이드 이펙트 방지가 마음에 드는데

방지를 위해 두번 요청하는 (우리가 부작용이라 생각했던) 동작이

아래와 같은 경우에 발생한다고 한다.

렌더링 단계 생명주기 메서드는 클래스 컴포넌트의 메서드를 포함

1. constructor
2. componentWillMount (or UNSAFE_componentWillMount)
3. componentWillReceiveProps (or UNSAFE_componentWillReceiveProps)
4. componentWillReceiveProps (or UNSAFE_componentWillReceiveProps)
5. getDerivedStateFromProps
6. shouldComponentUpdate
7. render
8. setState

 

 

결론

아직 대부분 class component의 생명주기 메서드들에 해당하는 내용으로 보인다.

그러므로 나는 일단 strict mode는 해제하도록 하겠다.

 

 

참고

728x90
반응형
728x90

Recoil 배경

Recoil은 Facebook이 만든 상태관리 라이브러리로

리액트의 상태관리를 위해 만들어진 라이브러리다.

매우 React스러운 Recoil에 대해 알아보자.

 

Recoil 구성요소

Recoil은 크게 2가지 요소로 나뉜다.

Atom과 Selector

 

Atom은 상태의 단위로, 상태를 정의하거나 불러올 수 있다.

Atom이 업데이트 되면 Atom을 구독하는 컴포넌트들이 업데이트된다.

Atom은 고유한 key와 default 값을 갖는 객체로 구성된다.

const phoneState = Atom('UserPhone', {
  key: 'Phone',
  default: ''
});

const [phone, setPhone] = useRecoilState(phoneState);
// const phone = useRecoilValue(phoneState);
// const setPhone = useSetRecoilState(phoneState);

useEffect(() => {
  getUserPhone()
}, [])

const getUserPhone = async () => {
  const { data } = await axios.get(uri);
  setPhone(data);
}

 

Selector는 고유한 key와 get과 set으로 구성되어 순수 함수로써 동작한다.

순수함수로써 동작한다는 것은 같은 입력이 주어진다면 항상 같은 결과를 반환해야 한다는 것이다.

내부의 다른 요소로 인해 결과가 바뀌는 등의 상황이 없어야 한다는 것.

const phoneState = Atom('UserPhone', {
  key: 'Phone',
  default: ''
});

const getPhoneSelector = () => selector({
  key: 'UsePhone',
  get: async ({ get }) => {
    try {
      const { data } = await axios.get(uri);
      return data;
    } catch(error) {
      throw error;
    }
  },
  set: ({ set }, newValue) => {
    set(phoneState, newValue);
  }
})

반드시 비동기 요청을 위해 쓸 필요는 없지만

위와 같이 비동기 요청을 위해 사용할 수 있다.

 

비동기 요청 로직을 분리하여

요청 후 결과를 atom에 set하는 형태를 갖출수도 있지만,

selector를 이용해  응집도를 높일 수 있다.

 

뿐만 아니라, selector는 캐싱 기능이 있다.

캐싱 기능의 장점은 이미 받아왔던 값을 기억하고 있어

같은 응답을 보내는 api call에 대해서는 추가적으로 요청하지 않아

성능적으로 훨씬 유리하다.

 

* 캐싱과 관련해 주의할 부분

사용자가 항상 최신의 데이터를 확인해야 하는 화면에서

캐싱 기능은 데이터 무결성을 초래하는 위험 요소가 될 수 있다.

따라서, 항상 최신의 데이터를 확인해야 할만한 상황에 대해서 미리 파악 해두어야한다.

예를들어

  - 사용자가 화면을 보고있을 때

  - 페이지가 전환되며 새 페이지에 진입했을 때

  - 페이지 전환없이도, 무언가 추가적인 데이터를 요청할 때

등이 있을 수 있다.

이러한 상황 외에는 적극적으로 캐싱 기능을 활용한다면

자원을 아끼고 효율을 높일 수 있을 것이다.

Recoil 비동기 처리?
Recoil에서도 비동기 요청 처리에 있어

Redux에서 처럼 error, loading 등의 상태를 확인할 수 있을까?

selectoruseRecoilValudLoable을 함께 쓴다면 가능하다.

 

const phoneLoadable = useRecoilValueLoadable(getPhoneSelector);

phoneLoadble.state
// hasValue, hasError, loading 세가지의 현재 상태를 갖음

phoneLoadable.contents
// hasValue 상태 일때: value 정보
// hasError 상태 일때: error 정보
// loading 상태 일때: Promise

 

참고1

참고2

728x90
반응형
728x90

누군가 내게 이런 질문을 해주었고,

다시 한번 고민해보고 공부해볼 좋은 기회가 되었다.

(보다 정확한 공부를 위해 React Docs를 살펴보았다.)

 

useEffect

둘을 비교하기 위해서는 useEffect를 먼저 이해할 필요가 있다.

 

useEffect를 이렇게 설명할 수 있을것 같다.

1) 특정 조건이 발생할 때(인자),

2) 지정된 명령을 수행하라(실행함수).
3) 컴포넌트가 제거될때 수행할것이 있다면 return을 작성하라.

 

(구조)

useEffect(실행 함수, 인자);


(사용 예시)

useEffect(() => {

}, [])

useEffect(() => {
  return () => {
  }
}, [])

useEffect(() => {
  return () => {
  }
}, [state.user])

 

 

useEffect의 동작 기준

1. 인자가 빈 배열인 경우: 모든 레이아웃 배치와 그리기를 마친 후 실행됨

2. 인자가 빈 배열이 아닌 경우: 배열의 state나 props가 변경될 때 실행됨

 

 

모든 레이아웃 배치와 그리기를 마친 후 실행된다고 했는데,

이 부분에 대해서 더 자세히 살펴보자.

 

useEffect 상세 실행순서

1) component render가 시작됨

2) rendered component가 화면에 그려짐

3) useEffect가 실행됨

 

자, 여기까지 살펴보고아래의 코드가 동작했을 때

사용자가 어떤 경험을 하게될지 예상해보자.

const [username, setUsername] = useState('')
useEffect(() => {
  setUsername('홍길동')
}, [])

return (
  <p>{username || '비회원'}</p>
)

사용자 경험에 어떤 문제가 있었을지

대충 눈치챘을 것이다.

 

사용자는 비회원이 아님에도

비회원을 목격한 뒤 홍길동이라는 이름이 보여지는 것을

목겨하게 될것이다.

 

 

useLayoutEffect

이러한 상황을 위해 useLayoutEffect가 존재한다.

 

useLayoutEffect 상세 실행순서

1) component render가 시작됨

2) useLayoutEffect가 동기적으로 실행됨

3) rendered component가 화면에 그려짐

4) (useEffect가 실행됨)

 

실행 순서 덕분에 위와 같은 상황에서는

useLayoutEffect를 쓴다면

조금 더 좋은 사용자 경험을 제공할 수 있다.

 

 

 

그렇다면 언제 useEffect를 써야할까?

일반적으로는 useEffect를 쓰는것이 퍼포먼스 면에서 유리하다.

 

왜냐면 위에서 언급했듯이 useLayoutEffect는

동기적으로 동작하기 때문에 퍼포먼스면에서는

권장되지 않기 때문이다.

 

일반적으로 useEffect를 사용하도록 하며

특히나 구독이나 이벤트 핸들러의 설정 등의 상황에서는

useEffect를 통해 필요한 시점에서만 실행되도록 하고

메모리 누수 방지를 위해 return을 통해 구독 해지시키는 것이 좋다.

 

 


(번외)

그 외에 react를 처음 사용하다보면

한번쯤 useEffect로 인해 겪게되는 이슈가 있다.

 

"어? 왜 자꾸 상태 값이 초기값으로 나오지?"

 

Docs를 자세히 보면 위 문제의 원인을 확인할 수 있다.

원인은 바로 useEffect에서 다루는 state, props가 인자에서 누락되었기 때문이다.

(useEffect에서 변경된 state,props를 확인하려면 인자에 useEffect에서 다루는 모든 state,props가 있어야함)

 

이것을 달리 말하면,

useEffect에서 인자로 빈 배열을 받았다면

useEffect에서 다루는 state나 props는 항상 초기값을 갖는다는 뜻이다.

 

심지어 이 사항은

useEffect에서 호출하는 함수에도 모두 적용되는 사항이니

명확히 알고 사용해야한다.

 

 

 

(수정할 사항이 있다면 제발 알려주세요. 저에게 큰 도움이 됩니다.)

728x90
반응형
728x90

React 개발을 하다보면

state, props로는 depth가 너무 깊어지는

props drilling문제가 발생하는 시점이 온다.

 

또한,

단계별 진행이나 페이지 변경 후에도 상태를 유지해야 하는 List뷰 등에서

어려움을 겪게된다.

 

이러한 문제들을 해결해주는 것이

global state관리를 도와주는 라이브러리들이다.

그 중에서 local global state라고 부르도록 하겠다.

 

주요 상태관리 라이브러리

1. Context API

2. Recoil

3. MobX

4. Redux

 

위 4가지 정도가 있는데,

1에서 4로 갈수록 러닝커브가 높다고 생각하는 바이다.

그럼에도 각 장,단점이 있으니 알아보도록 하자.

 

1. Context API

ContextAPI는 4가지 중에 유일하게
React 내장 상태관리 기능이다.

즉, 별도의 라이브러리 설치 없이 사용 가능하다는 말이다.

 

Docs를 보고 처음 사용 하는 사람도
쉽게 적용할 수 있는 수준이니

러닝커브 또한 낮다고 할 수 있다.

 

ContextAPI가 처음 나왔을때는

언어설정이나 색상테마 등

잘 변경되지 않는 상태를 관리할때 쓰였다.

 

그러나 지금은

전역상태 관리로 사용하던

지역상태 관리로 사용하던

개발자의 자유일뿐

상태 관리 기능으로써 충분한 지원을 하고있다.

 

참고

 

그럼에도 다른 라이브러리들을

추가 설치해 사용하는 이유가 있다.

 

 

2. MobX

언급할 내용은 MobX뿐 아니라

Redux, Recoil등 다른 라이브러리들을

사람들이 사용하는 이유이다.

 

4가지 모두 상태관리라는 컨셉을 가지고 있지만 그 안에서 나뉘는 개별 특징들이 있다.

그중에 내게 맞는 특징을 갖는 라이브러리를 택하면 되는것이다.

 

MobX는 전역상태관리 기능을 제공한다.

뿐만 아니라 상태 업데이트 로직을 View Component 밖에서(코드 분리) 할 수 있도록 도와준다.

Redux에 비해 적은 보일러 플레이트 코드, 직관적인 코드를 갖는다는 특징도 있다.

 

다만, MobX에서는 여러개의 Store를 둘 수 있는데

그로인해 예상치 못한 업데이트 등이 발생할 수 있다는 단점아닌 단점이 있다.

 

3. Redux

Redux도 MobX와 마찬가지로

전역상태관리 + 상태 업데이트 로직 분리를 돕는다.

 

하나의 Store를 갖는 특징으로

상태가 업데이트 될때 정확하게(직관적으로)

해당하는 상태를 갖는 컴포넌트들만 업데이트 된다는 장점이 있다. (유지보수의 편안함)

 

다만, Redux는 비동기 처리를 위해

thunk, saga 등 추가적으로 라이브러리들이 붙게되고

보일러플레이트 코드가 너무 많아진다는 단점을 가지고 있다.

 

4. Recoil

최근 각광받고 있는 Recoil은

react를 개발/운영하고 있는 Facebook(Meta)에서 개발한 만큼

가장 react 친화적이라는 장점을 가지고있다.

뿐만 아니라 매우 사용 방법이 매우 간단하고 직관적이다.

 

ContextAPI가 상태를 일일이 만들어야 하는 과정을 가진데 반해

Atom을 통해 매우 간결하게 상태관리를 할 수 있다.

캐싱을 통한 최적화 기능은 보너스이다.

 

 

 

최근,

이직 준비를 하며 많은 기업들에서 Recoil을 도입하고 있는것을 보게되었다.
사이드 프로젝트에 적용해 공부중인데 매우 합리적인 선택지라는 생각이 든다.

 

현직 개발자들은 대부분 현명하고, 최선의 선택을 하기위해 노력하는 만큼

Recoil이 사랑받는데는 분명히 이유가 있다.

 

 

 

(수정할 사항이 있다면 자유롭게 알려주세요. 저에게 큰 도움이 됩니다.)

728x90
반응형
728x90
<textares
  style={{ resize: 'none' }}
  onKeyDown={e => {
    const numberOfLines = (e.target.value.match(/\n/g) || []).length + 1;
    if (e.which === 13 && numberOfLines >= 3) {
      e.stopPropagation();
      e.preventDefault();
      return false;
    }
  }}
  onChange={onChange}
/>
728x90
반응형
728x90

1. LifeCycle

class 기반 react를 오랫동안 써왔다.

그동안은 LifeCycle이 굉장히 중요시 여겨지며

LifeCycle을 기반으로 적재 적소에 함수들을 호출해 개발했다.

 

Hooks로 기반을 변경한 뒤,

useEffect에 의존한 단조로운 LifeCycle위에서 개발하게 되었다.

 

그런데, 이 useEffect에서 호출되는 함수들은

어느 시점에 호출되는걸까?

 

useEffect : render -> useEffect -> (re-rendering)

 

DOM의 레이아웃 배치와 페인팅이 끝난 후,

useEffect의 사이트 이펙트에 해당하는 함수들이 호출된다.

 

여기서 아래와 같은 문제가 발생할 수 있다.

1) 사용자가 짧게나마 빈 페이지를 보게됨

2) 기본 값으로 구성된 페이지를 보게됨

 

대부분의 웹 페이지들이

페이지가 열릴때

데이터를 비동기적으로 불러와

state의 갱신을 동해 화면을 re-rendering해서

사용자에게 제공된다는 것을 생각하면

useEffect를 이런식으로 활용하는것에는

어느정도 문제가 있다고 볼수있겠다.

 

그러면 어떻게 하면 좋을까

하고 알아보니

useLayoutEffect라는 것이 있었다.

 

useLayoutEffect : useLayoutEffect -> render -> (useEffect) -> (re-rendering)

 

useEffect와 달리

DOM이 레이아웃 배치 및 페인팅을 하기 전에

즉, render 보다도 먼저 호출된다.

 

useLayoutEffect를 통해

완벽하게 위의 1), 2) 문제를 제어할수는 없어도

좀더 효과적으로 서비스를 제공할수는 있겠다.

 

(위 문제를 완벽하게 해결하려면 SSR방식의 next를 써야할듯하다.)

 

 

 

2. useState

useState는 단연코 가장 많이 쓰이는 hooks일것이다.

프로젝트를 진행하다보니

코드를 올바르게 작성한듯한데

state가 제때 갱신되지 않아서 의도와 다르게

결과값이 나오는 경우가 심심치않게 보였다.

 

이는 동일블록 내에서 setter를 사용할 때

Closer 구조로 인해 발생하는 문제로,

class기반의 react를 사용할때도 가지고 있던 문제다.

 

class기반에서도 

setState(prevState => ({ ...prevState, type: value }));

위와 같이 처리하곤 했었는데

hooks에서도 마찬가지로 위와 같은 방식으로

해당 문제를 해결할 수 있다.

 

예를 들어,

const [list, setList] = useState([]);


const updateList = (data) => {
  // setList(list.concat(data)); <-- X
  
  setList(prevList => {
    const newList = list.concat(data);
    return newList;
  });
}

이런식으로 처리하면 된다.

728x90
반응형
728x90

나는 그동안 redux를 쓰며 의문을 가졌다.

사실 쓰기 매우 꺼려지기까지 했다.

 

global state 관리를 위해

redux를 쓰는것은 동의

 

다만, 그 많은 코드를 써가면서까지

비동기 요청을 하고

응답된 데이터를 state로 관리하고

해야되는걸까?

 

실제로 서비스를 개발하면서

gloal state로 관리해야될 데이터는

많지 않았던것같다.

 

때에 따라서는

global state로 관리되는 데이터들에대한

최신화 문제를 겪기도했다.

 

이번에 새로운 앱 MVP 제작을위해

자료조사를 하던도중

이 포스팅을 보고

"react-query" 를 도입해보았다.

 

react-query는 global state를

server-state와 client-state라는

두개의 개념으로 분리해서 바라본다.

 

나는 이에 크게 동의했다.

 

예를들어, 포스팅 목록을 불러왔을때는

글로벌로 관리해야한다고본다.

(사용자가 새로고침하기 전까지는)

 

대신, 포스팅 단일 데이터는

굳이 글로벌로 관리할 필요가 있을까?

아니라고 생각한다.

 

global state라는 개념을 도입하게된 배경은

[부모 -> 자식 -> 자식 ->  자식 -> 자식]

으로 props를 전달하는데 불편함이 있어서이며,

불필요한 fetch를 방지하기 위함이다.

 

포스팅 단일 데이터호출 및 렌더링에는

이런 문제가 있지도 않을텐대

그 많은 코드를 써가며 global state로

관리할 이유가 없다고 생각하는것이다.

 

이런 측면에서 접근했을때

경우에 따라 global state로 관리해야 하는 경우에만

Redux를 이용하고

일반적으로는 state 그 자체면 충분하다.

 

특히나, 비동기 요청을 위해

redux-sage, redux-thunk등을 쓸 이유?

는 없어진다고 생각한다.

 

아직까지는 대규모 프로젝트에서

관리의 용이성을 위해 redux + redux-saga를 많이 쓴다곤 하지만

react-query가 어느정도 궤도에 이르면

redux + react-query 구조로 가지는 않을까하고

조심스럽게 예상해본다.

 

이제 만들고있는 앱에

redux도 붙이러 가야겠다

 


 

redux-saga, redux-thunk가

너무나도 싫어서

기피하고 외면하던 게으른 개발자가

 

마음에 쏙드는 신문물을 발견해

작성한 포스팅입니다.

728x90
반응형
728x90

React기반인 서비스를 운영중인 회사의 서비스 페이지에 Fullpage.js + aos를 적용해보았습니다.

아주 쉽고 빠르게 잘 붙었지만

이게 웬걸...

 

로딩만 했다하면 도저히 원인 파악이 어려울 정도의 상황이 발생했습니다.

스크롤 위치가 이상한데 잡히면서 컴포넌트들이 나타나지 않았습니다.

 

aos가 나타나는 trigger에 문제가 있다고 짐작할뿐 대체 왜 이런 현상이 계속되는가...

를 3시간을 추적에 추적을했습니다.

 

그러다 Lazy loading이라는 키워드를 발견했고

서비스페이지만 Lazy loading을 제거했습니다.

 

와우 너무나도 정상적으로 동작..?!

 

이렇게 간단한 이슈인것을 몇시간을 헤맸네요.

Lazy loading을 했을때 왜 해당 이슈가 발생하는지 근본적인 원인에 대해 탐구하는 포스팅을 다음 글에서 이어가도록 하겠습니다.

 

728x90
반응형
728x90

Input에서 한글입력을 막는 방법에는

여러가지 접근 방법이 있다.

 

절대적인 방법을 발견하여 메모해둔다.

 

1. Input타입이 text인 경우에는 정규식을 통해 체크하여 원하는 문자열 입력방지를 실시할수있다.

예를들어, 숫자만 입력받고싶다면

 

const regExp = /[a-z|ㄱ-ㅎ|ㅏ-ㅣ|가-힣]/g;

 

이런식의 정규식으로 입력값을 체크해서 입력을 방지하면 된다.

그리고 실제로 가능한 방법이다.

 

 

그러나,  우리는 사용자의 입력 편의성을 위하여 input타입을 number로 잡아줘야한다.

그때는 숫자외 다른 입력값을 입력했을때 정규식이 바보가된다.

인지를 못한다.

 

왜 그런가하고 타겟의 값을 콘솔에 찍어보았다.

console.log(e.target.value)

결과: 

분명히 실제로는 "123ㄱ"라고 화면에 나타나 있지만 결과에는 빈값이 출력되었다.

그러니 정규식이 캐치할래야 할수가 없는 상황이었다.

 

2. 정규식이 안먹히면 어떻게하지? 키코드를 사용해야하나?

원하는게 안먹히면 우리들은 발상의 전환을 시도한다.

하지만, 헛탕일 켤뿐 안된다는것을 미리 밝히는 바이다.

 

3. 그래서 어떻게 해야되는가?

input태그에  onchange이벤트를 걸었다면 아래와 같은 방법으로

숫자입력만 받는 방법이 가능하다.

const isNotNumber = () => {
  const regExp = /[a-z|ㄱ-ㅎ|ㅏ-ㅣ|가-힣]/g;
  return regExp.test(value);
}

<input 
  type='text'
  onchange={e => {
    if (e.nativeEvent.data && isNotNumber(e.nativeEvent.data) {
      e.preventDefault();
      return null;
    }
    
    ...something
  }}
/>

 

매우 아름다운 방법으로 생각된다.

e.nativeEvent.data에는 놀랍게도 내가 지금 입력한 그 키값이 나타난다.

3이라면 3, t라면 t가 말이다.

backspace는 null이 나타난다. 따라서 null인 경우에는 허용을 해줘야한다.

 

 

사실 그동안 숫자만 입력받는 코드를 짜뒀다고 했는데

자꾸 한글이 입력된다는 리포트를 받아서 당황스러웠던적이 있다.

코드는 거짓말을 하지않는다...

나의 실책일뿐~~ ㅎㅎ

 

이 방법이 유효하지 않다거나 더 좋은방법이 있다면 댓글을 달아주시면 정말 감사하겠습니다.

728x90
반응형