{}연습장
대시보드학습스킬 경로문제모의고사라이브 코테복습데이터
← 학습 목록로드맵에서 보기
Lv.0 입문

문자열

문자 순회·카운팅·파싱. JS 문자열은 불변이라는 성질과 인덱스 한 방향 전진이 핵심입니다.

지문 신호“문자열이 주어집니다”“회문”“규칙대로 변환”
0/8 해결
1

눈으로 보기

재생을 누르고 단계별로 동작을 따라가세요
레
벨
수
수
레
벨
레
1/12"레벨수수레벨레" — 양끝에서 한 글자씩 비교합니다. (예제 1)
■ 앰버 = 비교 중인 두 글자회문 검사 — 양끝 수렴 순회
2

개념 이해

핵심 패턴 코드는 손으로 따라 쳐 보는 것을 권장
1 / 8

비유로 이해하기

문자열은 이미 인쇄되어 나온 영수증입니다. 몇 번째 글자인지만 알면 눈으로 바로 짚을 수 있어 읽기는 빠릅니다. 하지만 가운데 글자 하나를 고치려고 해도 지우개로 덧쓸 수가 없어서, 종이를 새로 한 장 뽑아 처음부터 다시 인쇄해야 합니다. 고칠 일이 잦다면 영수증을 계속 다시 뽑는 셈이라 비용이 쌓입니다.

문자열은 글자를 번호로 읽는 것은 빠르지만, 한 글자라도 바꾸면 새 문자열이 통째로 만들어집니다(불변).

  • 읽기: 영수증[2] → 세 번째 글자를 바로 본다 (빠름)
  • 고치기: "라면"의 뒤에 "먹" 붙이기 → "라면"을 복사한 새 종이에 "먹"까지 인쇄 (느림)

"라"에서 시작해 한 글자씩 이어 붙여 "라면먹자"를 만드는 과정을 손으로 따라가 봅니다.

단계 하는 일 실제로 벌어지는 일 이번에 복사한 글자 수
시작 s = "라" 1글자 문자열 하나 1
1 s += "면" "라"를 새 종이에 옮겨 적고 "면" 추가 → "라면" 2
2 s += "먹" "라면"을 다시 옮겨 적고 "먹" 추가 → "라면먹" 3
3 s += "자" "라면먹"을 다시 옮겨 적고 "자" 추가 → "라면먹자" 4
끝 "라면먹자" 원하는 결과는 나왔지만 합계 1+2+3+4 = 10

결과는 4글자인데 실제로는 10글자를 옮겨 적었습니다. 10만 글자를 이렇게 이어 붙이면 약 50억 글자를 옮기게 되어 시간 초과가 납니다. 그래서 조각을 배열에 모아 두었다가 마지막에 한 번만 join("") 합니다.

문자열 문제의 기본 골격은 "정규화하고 → 배열로 펼쳐 훑고 → 다시 문자열로 모은다"입니다.

  1. 비교 전에 형태를 통일합니다 — 대소문자·공백·불필요한 기호를 먼저 정리합니다.
const normalized = s.toLowerCase().trim(); // 원본은 그대로, 새 문자열이 생깁니다
  1. 글자 단위로 다루려면 배열로 펼칩니다 — 문자열 자체는 고칠 수 없기 때문입니다.
const chars = [...s];  // s.split("")도 같은 뜻
chars[0] = "A";        // 배열이므로 수정 가능
  1. 결과는 배열에 모았다가 마지막에 한 번만 합칩니다.
const parts: string[] = [];
parts.push("가", "나");
const answer = parts.join(""); // "가나"

전체 코드 — "알파벳과 숫자만 남겨 회문인지 판정하기":

function solution(s: string): boolean {
  // 1단계: 소문자로 통일하고 알파벳·숫자만 남기기
  const cleaned = [...s.toLowerCase()].filter((ch) => /[a-z0-9]/.test(ch));
  // 2단계: 양끝에서 가운데로 좁히며 비교
  let l = 0;
  let r = cleaned.length - 1;
  while (l < r) {
    if (cleaned[l] !== cleaned[r]) return false;
    l++;
    r--;
  }
  // 3단계: 끝까지 어긋나지 않았으면 회문
  return true;
}

문자열 문제는 크게 세 부류입니다: 순회·인덱스 조작(회문, 뒤집기), 카운팅(애너그램, 빈도 — 해시와 결합), 파싱·변환(규칙대로 해석). 알고리즘보다 JS 문자열의 성질을 아는 것이 먼저입니다.

  • "뒤집어서 같은지", "앞뒤가 대칭인지" → 양끝 순회형입니다.
  • "글자를 재배열하면 같은 단어가 되는지", "가장 많이 나온 문자" → 카운팅형이고 해시나 크기 26 배열을 씁니다.
  • "규칙에 따라 해석하라", "괄호/토큰을 읽어라", "형식을 바꿔 출력하라" → 파싱·변환형이고 시뮬레이션에 가깝습니다.
  • 이어 붙이기를 반복하는 루프가 보이면 길이 제약을 확인합니다 — 10만 이상이면 배열에 모아야 합니다.

먼저 도구 상자입니다. 이 목록만 손에 익어도 대부분의 문자열 문제는 도구가 부족해서 막히지 않습니다.

s.length, s[i], s.slice(l, r)        // 읽기 (slice는 r 미포함)
s.split(""), [...s]                   // 문자 배열로
arr.join("")                          // 배열 → 문자열
s.charCodeAt(i), String.fromCharCode(65)  // 'A'=65, 'a'=97, '0'=48
s.toLowerCase(), s.trim()
s.split(" ")                          // 단어 분리 — 연속 공백 주의: split(/\s+/)
Number(s), parseInt(s, 2)             // 수 변환 (2진수 등 진법)
s.localeCompare(t)                    // 사전순 비교 (한글 포함)
// 1) 불변 함정 회피 — 이어붙이기는 배열에 모아서
const parts: string[] = [];
for (const piece of pieces) parts.push(piece); // s += piece 반복은 O(N²)
return parts.join("");
// 2) 알파벳 카운팅 — 크기 26 배열
const cnt = Array(26).fill(0);
for (const ch of s) cnt[ch.charCodeAt(0) - 97]++;
// 3) 파싱 — 상태를 들고 한 글자씩
let i = 0;
while (i < s.length) {
  if (isDigit(s[i])) {
    // 숫자 토큰을 끝까지 읽기
  } else {
    // 그 밖의 토큰 처리
  }
  i = 다음_토큰_시작;
}

파싱 문제는 "토큰이 무엇인지"를 먼저 정의하고, 인덱스 i를 한 방향으로만 전진시키면 꼬이지 않습니다.

  • s[i] = "x" — 조용히 무시됨(불변). 배열로 바꿔 조작 후 join
let bad = "abc";
bad[0] = "z";                 // TS는 컴파일 에러, JS는 조용히 무시 — 값은 그대로 "abc"
const chars = [...bad];
chars[0] = "z";
const good = chars.join("");  // "zbc"
  • split(" ")에 연속 공백 → 빈 문자열 원소 발생. split(/\s+/).filter(Boolean) 또는 trim 선행
"a  b".split(" ");                      // ["a", "", "b"] — 빈 칸이 섞임
"a  b".split(/\s+/).filter(Boolean);    // ["a", "b"]
  • slice(l, r)의 r 미포함을 잊음 — "r번째까지"면 slice(l, r + 1)
  • 대소문자 혼재 입력을 정규화 없이 비교
  • 숫자 문자열 비교를 <로 — "9" > "10" (사전순). 길이가 다르면 수로 변환하거나 길이 먼저 비교
"9" < "10";                    // false — 첫 글자 '9'와 '1'을 비교하기 때문
Number("9") < Number("10");    // true
  • 왜 불변인가: 문자열은 여러 변수에서 동시에 참조되는 일이 흔합니다. 수정을 허용하면 한쪽에서 고친 값이 다른 쪽까지 바뀌므로, 엔진은 아예 수정을 막고 같은 내용의 문자열을 안전하게 재사용(인터닝)합니다. 대신 새 문자열을 만들 때마다 글자 복사 비용이 듭니다.
  • s += x 반복이 위험한 이유: 이론상 매번 전체 복사라 N번 이어 붙이면 O(N²)입니다. V8은 이어 붙인 결과를 즉시 복사하지 않고 조각을 잇는 중간 표현(로프, ConsString)으로 지연시키는 최적화가 있지만, 중간에 길이를 읽거나 인덱스로 접근하면 그 자리에서 평탄화되어 다시 비용이 발생합니다. 배열에 모아 join하는 방식이 예측 가능하게 O(N)입니다.
  • length는 글자 수가 아니라 UTF-16 코드 유닛 수: 한글 완성형은 1이지만 이모지나 일부 보조 문자는 서로게이트 페어로 2로 셉니다. s.length나 s[i]는 이때 글자를 반 토막 낼 수 있고, [...s]와 for (const ch of s)는 코드 포인트 단위로 순회하므로 안전합니다.
  • 문자 코드 산술: 'a'=97, 'A'=65, '0'=48을 기준으로 charCodeAt(0) - 97은 알파벳 인덱스, (idx + k) % 26은 시저 암호식 회전이 됩니다. 숫자 문자 하나를 수로 바꿀 때도 ch.charCodeAt(0) - 48을 쓸 수 있습니다.
  • 사전순 비교의 두 갈래: < 연산자는 UTF-16 코드 유닛 비교라 모든 대문자가 소문자보다 앞섭니다("Z" < "a"). 사람이 기대하는 사전순, 특히 한글이 섞인 정렬은 localeCompare를 씁니다.

시각화(회문 검사)로 양끝 순회를 본 뒤, 뒤집기·회문 → 카운팅(애너그램) → 파싱·규칙 변환 순으로. 파싱형은 시뮬레이션 유형의 문자열 버전이라 함께 늘어납니다.

3

문제로 확인

난이도 순서대로 8문제 — 막히면 개념으로 돌아왔다 다시
1출입 태그 대칭 구간#003다음 풀 문제쉬움2공통 상위 디렉터리 찾기#013쉬움3비밀번호 유효성 검사#059쉬움4자릿수 더하기#061쉬움5회전 로터 통신문#071쉬움6문장 내 각 단어 뒤집기#080쉬움7알림 문구 템플릿 전개#100중간8드론 기동 매크로 전개#113어려움
다음 토픽 · Lv.0 →수학