분류 전체보기 (165)

05
31

진짜 docs 100번 이상은 정독했다. 씁쓸하다.

1. 문제의 발단

  • 평소에 매우 자주 사용하는 지도 API다. (... 였다) 전투적으로 약 8달 이상은 사용했던 네이버 지도에서 쉽고도 어렵게 사용했던 Marker 시스템을 정리해 보려고 한다.

2. 기본적인 사용법

const loadScript = () => {
  return new Promise((resolve) => {
    if (document && document.querySelectorAll('#naver-map-sdk').length === 0) {
      const script = document.createElement('script');
      script.id = 'naver-map-sdk';
      script.src = `https://openapi.map.naver.com/openapi/v3/maps.js?ncpClientId=${NCP_CLIENT_ID}&submodules=geocoder,panorama`;
      script.onload = () => {
        resolve(true);
      };
      document.head.appendChild(script);
    } else {
      resolve(true);
      console.log('script already loaded');
    }
  });
};
  • 위와 같이 script를 심어준다. await로 지도 init시 호출하게 된다.
 const initMap = async (containerId: string, isActiveDistance: number) => {
	// 스크립트 로드
	await loadScript();

    // 지도 생성시 설정해 줄 옵션이다.
    const initialMapOptions = {
      center: [point.longitude, point.latitude],
      zoom: 16,
      minZoom: 12,
      maxZoom: 22,
      zoomOrigin: [point.longitude, point.latitude],
      pinchZoom: false,
      mapDataControl: false,
      disableDoubleClickZoom: false,
      disableDoubleTapZoom: false,
      logoControlOptions: {
        position: naver?.maps?.Position?.BOTTOM_LEFT,
      },
      mapDataControlOptions: {
        position: naver?.maps?.Position?.BOTTOM_CENTER,
      },
      scaleControlOptions: {
        position: naver?.maps?.Position?.BOTTOM_LEFT,
      },
    };

    const map = new naver.maps.Map(containerId, initialMapOptions) as any;

    new naver.maps.Circle({
      map: map,
      center: [point.longitude, point.latitude],
      radius: isActiveDistance + 100,
      fillColor: '#ffffff',
      fillOpacity: 0.2,
      strokeOpacity: 0,
    });

    // 마커생성은 함수로 만들어서 쓰고 있다.
    const pinMarker = makeIconMarker(map, [point.longitude, point.latitude], 'pin');

    // 지도 옵션 (마커나 지도에 커서를 바꿔줌)
    pinMarker.setCursor('default');
    map.setCursor('default');

    return map;
  };

  useEffect(() => {
    initMap('mainMap');
  }, []);
  • 이런 식으로 지도를 생성해 주고,  map이 들어갈 div에 id 값을 알맞게 준다.
  • 각종 지도 옵션 및 마커, 도형 등의 옵션은 docs를 참고한다. (https://navermaps.github.io/maps.js.ncp/docs/)

3. Marker에 대한 고찰

  • 지도에서 특정 마커를 지우거나 선택등을 하려면 그 특정 마커 요소 자체를 정확히 알고 가지고 있어야 한다.
  • 이때, state로 마커를 가지도 다닐 수는 없다.
  • 예를 들어, maker.setMap(null)로 특정 마커를 지울 수 있다. 이때, state에 마커를 넣어놓고 지우려고 하면 올바르게 동작하지 않는다.
  • 이때는 useRef를 사용한다.
const markerRef = useRef<naver.maps.Marker>();

    const marker = makeMarker(map, point, {
      value: 100
    });

markerRef.current = marker;

useEffect(() => {
    markerRef.current?.setMap(null);
    
    const marker = makeMarker(map, point, {
      value: 200
    });
    
    marker.setMap(mapRef.current);
    markerRef.current = marker;
    }
  }, [isActive]);
  • isActive가 변경될 때마다, 기존의 value 100의 마커는 사라지고, value 200을 가진 마커가 생성된다. (isActive가 단 한 번만 바뀐다는 전제 하에 이렇게 만들 수 있지만 급조한 코드라 어색하다)
  • 마찬가지로 마커 뿐 아니라 map 객체 자체나 도형들과 같은 지도 위 요소들도 다 useRef로 관리가 가능하고, 이를 list형태로 저장하여 사용할 수 도 있다.
COMMENT
 
05
29

ㅇ리소스 누수를 막아봅시다

1. useMemo?

  • input이나 state 값이 바뀌는 경우 react는 컴포넌트들을 리렌더링 한다. 
  • 이때, 변화가 없는 값을 계산하는 함수일 경우 다시 호출 될 필요가 없다.
  • 이럴 때, 최적화를 위해 사용되는 것이 useMemo이다.
  • 진정한 뜻은 'memoization'에서 온 것으로, 메모리에 이전 값을 저장해서 변경이 없는 값이면 불필요하게 계산을 하지 않게 된다. 

2. 예시

import "./styles.css";
import { useState } from "react";

export default function App() {
  const [num, setNum] = useState(100);
  const [value, setValue] = useState("");

  const getNum = () => {
    console.log("num : ", num);
    return num;
  };

  const item = getNum();

  return (
    <div className="App">
      <h1>num : {item}</h1>
      <button onClick={() => setNum((prev) => ++prev)}>Add num</button>
      <h1>value : {value}</h1>
      <input onChange={(e) => setValue(e.target.value)} />
    </div>
  );
}
  • 매우 간단한 코드이다. 당근, input의 state인 value가 바뀌면 전혀 관계없는 num를 알려주는 getNum도 호출되므로, num은 바뀌지 않았지만 불필요하게 함수가 호출된다.

리소스가 줄줄센다

  • 하지만 useMemo를 사용해서 바꿔줄 수 있다.
import "./styles.css";
import { useState, useMemo } from "react";

export default function App() {
  const [num, setNum] = useState(100);
  const [value, setValue] = useState("");

  const getNum = () => {
    console.log("num : ", num);
    return num;
  };

  const item = useMemo(() => getNum(), [num]);

  return (
    <div className="App">
      <h1>num : {item}</h1>
      <button onClick={() => setNum((prev) => ++prev)}>Add num</button>
      <h1>value : {value}</h1>
      <input onChange={(e) => setValue(e.target.value)} />
    </div>
  );
}
  • useEffect를 사용할 때 처럼, num이 바뀔 때만 getNum을 호출하자. 그럼 value가 바뀔 때는 getNum은 호출되지 않는다.

땜빵전문

3. 실전팁

  • 각종 상황에 사용할 수 있지만, 가장 유용하다고 생각하는 부분은 아래 코드처럼 불필요한 컴포넌트 렌더링 자체를 피할 수 있다.
  • 슬라이드 메뉴, 파일을 넘나들지만 state로 관리되면 안되는 데이터들도 useMemo로 처리할 수 있다.
import { useMemo  } from 'react';

function Analysis() {
  const { path } = useParams();

  const content = useMemo(() => {
    switch (path) {
      case 'home':
        return <Home />;
      case 'login':
        return <Login />;
      case 'intro':
        return <Intro />;
      case 'Info':
        return <Info />;
      default:
        return null;
    }
  }, [path]);

  return (
    <div>{content}</div>
  );
}

export default App;

4. 주의사항

  • useMemo의 사용이 항상 옳지는 않다. 리렌더링을 방지하는 용도로 사용하는 것이 아니라, '리소스'를 절약해 주는 것에 의의가 있다.
  • 복잡한 함수가 아닐경우, 함수를 한 번 더 호출하는 것이 나은 경우도 있다. useMemo를 남발하면 코드 유지보수가 어려워진다.
  • 그리고 결국 메모리를 사용하는 작업이고, 결과가 바뀔 때마다 계산을 시도는 하기 때문에 너무 변동이 많은 곳에 사용하면 성능이 되려 떨어지게 될 수 도 있다.
COMMENT
 
02
21

1. 문제 설명

 

코딩테스트 연습 - 짝지어 제거하기

짝지어 제거하기는, 알파벳 소문자로 이루어진 문자열을 가지고 시작합니다. 먼저 문자열에서 같은 알파벳이 2개 붙어 있는 짝을 찾습니다. 그다음, 그 둘을 제거한 뒤, 앞뒤로 문자열을 이어 붙

programmers.co.kr

짝지어 제거하기는, 알파벳 소문자로 이루어진 문자열을 가지고 시작합니다. 먼저 문자열에서 같은 알파벳이 2개 붙어 있는 짝을 찾습니다. 그다음, 그 둘을 제거한 뒤, 앞뒤로 문자열을 이어 붙입니다. 이 과정을 반복해서 문자열을 모두 제거한다면 짝지어 제거하기가 종료됩니다. 문자열 S가 주어졌을 때, 짝지어 제거하기를 성공적으로 수행할 수 있는지 반환하는 함수를 완성해 주세요. 성공적으로 수행할 수 있으면 1을, 아닐 경우 0을 리턴해주면 됩니다.

예를 들어, 문자열 S = baabaa 라면

b aa baa → bb aa → aa 

의 순서로 문자열을 모두 제거할 수 있으므로 1을 반환합니다.

 

2. 제한 사항

  • 문자열의 길이 : 1,000,000이하의 자연수
  • 문자열은 모두 소문자로 이루어져 있습니다.

 

3. 입출력 예제

s result
baabaa 1
cdcd 0

 

4. 나의 접근 방식

  • 처음에는 split으로 모든 문자를 나누어 보았다.
  • 이후, 가장 처음 문자를 보고 두번째 문자와 비교해서 같으면 shift 두 번으로 빼버리려고 했다. 완전히 실패.
실패한 코드
function solution(s) {
    let splitStr = s.split('');

    for(let i = 0; i < splitStr.length; i++) {
        let first = splitStr[i];
        if(first === splitStr[i + 1]) {
            splitStr.shift();
            splitStr.shift();
        }
        else {
            // 맞지 않으면 다시 뒤로 넣어준다.
            splitStr.push(first);
        }
    }
    
    // 한번 이상 검사할 수 없기 때문에 무조건 0이 나온다.
    if(splitStr.length === 0) return 1;
    else return 0;
}​
  • 그런대 잘 생각해 보니 앞에서부터 할 필요가 없이, 스택처럼 쌓아서 뒤부터 보고 연속으로 같은 문자가 들어오면 뒤를 빼버리면 될 것이라고 생각했다.

 

4. 결과

function solution(s) {
    // 스텍으로 쓸 빈 배열을 만든다.
    let arr = [];
    
    for(let i = 0; i < s.length; i++) {
        // 각 요소들을 넣어주면서
        arr.push(s[i]);
        // 뒤부터 들어온 스텍에 똑같은 문자가 연속해서 들어 온다면,
        if(arr[arr.length - 1] === arr[arr.length - 2]) {
            // 두개를 빼 버린다.
            arr.pop();
            arr.pop();
        }
    }
    
    // 반복문을 다 돌고 나서, 스텍이 비어 있다면 전부 제거가 된 것이다.
    if(arr.length === 0) return 1;
    // 만약, 겹치지 않았다면 스텍에는 요소가 남아 있을 것이므로, 0을 반환한다.
    else return 0;
}

 

5. 개선점이 있다면?

  • 위의 방법으로도 왠지 풀 수 있을 것 같다...! (물론, 쉽지도 않고 효율도 떨어질 것이다)
  • 다양한 방식의 문제를 접하고, 어떻게 풀어야 할 지 더 다양한 방법으로 생각하는 것이 중요할 것 같다.
  • 조금 실수가 있었었다. 위의 문제는 split을 쓰면 올바르게 답을 작성해도 시간 초과가 나와서 테스트를 실패한다. 굳이, split을 쓰지 않고, 문자열 상태 그대로 반복문으로 해결할 수 있는 부분인데, pop과 같은 배열에서 사용할 수 있는 함수를 사용하려고 바꿨던 것이다. 이런 식으로 필요 없는 함수의 사용을 줄여 효율성을 생각하는 것도 중요한 부분이 될 것이다.
COMMENT
 
02
20

1. 문제 설명

 

코딩테스트 연습 - 구명보트

무인도에 갇힌 사람들을 구명보트를 이용하여 구출하려고 합니다. 구명보트는 작아서 한 번에 최대 2명씩 밖에 탈 수 없고, 무게 제한도 있습니다. 예를 들어, 사람들의 몸무게가 [70kg, 50kg, 80kg, 5

programmers.co.kr

무인도에 갇힌 사람들을 구명보트를 이용하여 구출하려고 합니다. 구명보트는 작아서 한 번에 최대 2명씩 밖에 탈 수 없고, 무게 제한도 있습니다.

예를 들어, 사람들의 몸무게가 [70kg, 50kg, 80kg, 50kg]이고 구명보트의 무게 제한이 100kg이라면 2번째 사람과 4번째 사람은 같이 탈 수 있지만 1번째 사람과 3번째 사람의 무게의 합은 150kg이므로 구명보트의 무게 제한을 초과하여 같이 탈 수 없습니다.

구명보트를 최대한 적게 사용하여 모든 사람을 구출하려고 합니다.

사람들의 몸무게를 담은 배열 people과 구명보트의 무게 제한 limit가 매개변수로 주어질 때, 모든 사람을 구출하기 위해 필요한 구명보트 개수의 최솟값을 return 하도록 solution 함수를 작성해주세요.

 

2. 제한 사항

  • 무인도에 갇힌 사람은 1명 이상 50,000명 이하입니다.
  • 각 사람의 몸무게는 40kg 이상 240kg 이하입니다.
  • 구명보트의 무게 제한은 40kg 이상 240kg 이하입니다.
  • 구명보트의 무게 제한은 항상 사람들의 몸무게 중 최댓값보다 크게 주어지므로 사람들을 구출할 수 없는 경우는 없습니다.

 

3. 입출력 예제

people limit return
[70, 50, 80, 50] 100 3
[70, 80, 50] 100 3

 

4. 나의 접근 방식

  • limit를 넘기지 않는 가장 큰 수 중에 2개는 people에서 가장 큰 수와 가장 작은 수를 더 하면 된다고 생각했다.
  • 정답은 나온다. 하지만 효율성 실패
실패한 코드
function solution(people, limit, count = 0) {
    // 가장 큰 수와 가장 작은 수를 구한다.
    let max = Math.max(...people);
    let min = Math.min(...people);

    // 제귀함수의 탈출 조건이다.
    if(people.length === 0) return count;

    // 둘을 더 했을때 limit보다 작으면 같이 제거한다.
    if(max + min <= limit) {
        count++;
        people.splice(people.indexOf(max), 1)
        people.splice(people.indexOf(min), 1)
    }

    // 보다 크다면 가장 큰 수만 제거해 준다.
    if(max + min > limit) {
        count++;
        people.splice(people.indexOf(max), 1)
    }

    return solution(people, limit, count)
}​
  • 대부분의 프로그래머스 문제들의 효율성 문제는 '정렬'이 안 된 상태로 계산해서 틀리는 경우가 많았다.
  • 그래서 일단 sort를 이용해 정렬을 해 놓고 생각해 보았다.
  • 로직은 맞는 것 같아서 이 방식대로 정렬한 후의 index를 이용해서 문제를 해결할 수 있었다.

 

4. 결과

function solution(people, limit) {
    let result = 0;
    // 정렬해 준다.
    people.sort((a,b) => b - a);

    // 각각의 index를 구해준다.
    // 당연히, 가장 앞이 가장 작은수, 가장 끝이 가장 큰 수가 된다.
    let minIndex = 0;
    let maxIndex = people.length - 1;

    // 가장 작은 수의 index가 가장 큰 수의 index를 넘길때 까지 반복하며,
    while(minIndex < maxIndex) {
        // 둘을 더 한 수를 limit와 비교한다.
        // 더 한 수가 limit보다 크면 minIndex만 늘려주고,
        let sum = people[minIndex] + people[maxIndex];
        if(sum > limit) minIndex++;
        // 넘지 않는 다면 가장 작은수와 가장 큰 수 둘다 보트에 탈 수 있으므로,
        // 같이 조절 해 준다.
        else {
            minIndex++;
            maxIndex--;
        }
        // 보트가 움직인 횟수를 더 해 준다.
        result++;
    }
    if(minIndex === maxIndex) result++;
    return result;
}
COMMENT
 
1 2 3 4 5 6 7 8 ··· 42