콜스택에서 발행한 RN 최적화 문서인 React Native Optimization를 2023년에 공부했던 것 같은데,
2025년 버전도 새로 나와서 이 참에 정리해 봤다.

 

예전에 웹 개발 팀원분이 React Native도 DOM 기반이에요? 라고 질문했을 때 아닌 건 알았지만, 제대로 설명 못했던 경험이 있는데, 리액트 리렌더링 모델 내용을 정리하면서 그때 생각이 났다.
 
아직도 React와 React Native의 차이를 모르는 분이라면 해당 내용을 읽어보면 좋을 것 같다.
react-dom은 별도의 렌더러라는 것을 ...


 

앱이 ‘빠르다’는 것은 무엇을 의미할까?
 
ex.
1940년대에 뉴욕의 한 오피스 빌딩에서 느린 엘리베이터에 대해 한 직원이 불만을 호소했는데,
건물 관리자는 엘리베이터 속도를 실제로 높이는 대신 근처에 거울을 설치했다.
 
어떻게 됐을까? 사람들은 거울을 통해 자신을 바라보면서, 대기 시간을 줄이고 주위를 분산시킬 수 있었다.
 
즉, ⭐️ 실제로 기술적인 개선도 중요하지만, 인지적인 성능 개선도 중요하다.
-> 스플래시 화면, 스켈레톤 UI, 애니메이션과 같은 트릭을 통해 사용자 만족도를 높일 수 있다.
 
인지적 성능이 중요한 만큼, 기술적인 개선도 중요하다.
 


 
 
사용자 경험을 올리는 데 중요한 지표 2가지

  1. TTI (Time To Interactive) : 앱이 실행된 후 앱을 얼마나 빨리 사용할 수 있게 되는지 측정하는 지표
    → 앱 초기 로드 외에도 특정 화면까지의 접근하는 데 걸리는 시간도 포함된다.
  2. FPS (Frames Per Second) : 초당 프레임 수로 앱이 스크롤, 스와이프 등 사용자 상호작용에 얼마나 부드럽게 반응하는지 측정하는 지표

 


PART 1

React Native의 Javascript 및 React를 최적화하여 FPS를 개선하는 기술

- 콜스택의 리액트 네이티브 개발자 100명을 대상으로 한 내부 설문조사에 따르면,
모바일, TV, 데스크톱 리액트 네이티브 앱에서 직면하는 성능 문제의 80%가 자바스크립트 측면에서 발생함.

React Native Thread 구조

 

React re-rendering model

⭐️ 렌더링 시점은 React가 결정하고, 렌더링 방법은 별도의 렌더러가 결정한다.
 
✅ React의 렌더링 및 업데이트 모델

  • React는 상태(state)에 따라 UI를 렌더링하고 업데이트하는 역할을 한다.
  • 핵심 구성 요소
    • Public API 정의
    • 크로스 플랫폼 기능
    • Reconciliation 알고리즘
      : 상태 변화에 따라 UI를 효율적으로 업데이트

✅ "What"과 "How"의 분리

  • React는 "무엇을 렌더링할지(what)"만 신경 쓰고,
  • 실제 "어떻게 렌더링할지(how)"별도의 렌더러가 담당함

    → 각 플랫폼에 따른 렌더러 예시:

    • react-dom: 웹용
    • react-native: 모바일(Android, iOS)
    • react-native-windows: 윈도우용
  • 이 구조 덕분에 하나의 컴포넌트 기반 코드로 다양한 플랫폼에서 동시에 UI를 구성 가능

✅ Virtual DOM과 실제 DOM의 비교 및 업데이트 (react-dom을 사용하는 경우)

  • React는 변경된 Model을 기반으로 새로운 Virtual DOM을 생성
  • 이전 Virtual DOM과 비교(diff) 후,
  • 변경된 부분만 패치(patch) 방식으로 실제 DOM에 반영
    → 최소한의 업데이트만 수행함 (성능 최적화)

✅ 컴포넌트가 다시 렌더링 되는 경우

  1. 부모 컴포넌트가 리렌더링 될 때
  2. 상태(state, hooks 포함)가 변경될 때
  3. props가 변경될 때
  4. context가 변경될 때
  5. 강제로 업데이트할 때 (forceUpdate() 등)
  • React는 공통의 Reconciliation 알고리즘을 통해 어떤 컴포넌트를 업데이트해야 할지 판단한다.
  • Reconciliation은 웹이든, React Native든 공통된 로직

 


 
 
다음은 JS와 React 코드를 프로파일링 하여 FPS 속도를 높이고, JS 메모리 누수를 추적하여 개선하는 방법에 대해 알아보자.

아이템 41 any 진화를 이해하기

요약

  • 일반적인 타입들은 정제되기만 하는 반면 (ex. 타입 좁히기), 암시적 any와 any[] 타입은 진화할 수 있습니다.
    이러한 동작이 발생하는 코드를 인지하고 이해할 수 있어야합니다.
  • any를 진화시키는 방식보다 명시적 타입 구문을 사용하는 것이 안전한 타입을 유지하는 방법 입니다.

 
1. 일반적인 타입들은 정제되기만 하는 반면, 암시적 any와 any[] 타입은 진화할  있습니다.

  • ❗️타입의 진화는 값을 할당하거나 배열의 요소를 넣은 ‘후’에만 일어나기 때문에, 편집기에서는 할당 다음 줄을 봐야 진화된 타입이 잡힌다.
//1. 배열의 any 타입 진화(evolve)
const result = [];  //any[]
result.push('a');  
result   //string[]
result.push(1);
result   //(string | number)[]
//2. 조건문에 따른 any 타입 진화(evolve)
let val;  //any
if(Math.random() < 0.5) {
    val = /hello/;
    val  //RegExp
} else {
    val = 12;
    val  //number
}

val  //number | RegExp
//3. 초깃값이 null 인 경우 any 타입 진화(evolve)
let tmp = null;  //any
try {
    somethingDangerous();
    tmp = 12;
    tmp  //number
} catch (e) {
    console.log('err!');
}

tmp  //number | null
  • any 타입의 진화는 "noImplicitAny": true 로 설정된 상태에서 변수의 타입이 암시적 any인 경우에만 일어난다. → 명시적인 경우 진화가 일어나지 않는다.
//명시적인 경우 진화가 일어나지 않는다.
let val2: any;  //any
if(Math.random() < 0.5) {
    val2 = /hello/;
    val2  //any
} else {
    val2 = 12;
    val2  //any
}

val2  //any

 
2. any를 진화시키는 방식보다 명시적 타입 구문을 사용하는 것이 안전한 타입을 유지하는 방법이다.

  • 암시적 any 상태인 변수에 어떠한 할당 없이 사용하려 하면 암시적 any 오류가 발생한다.
function range(start: number, limit: number) {
    const out = []; //암시적 any
    if (start === limit) {
        return out; //❌ Error:'out' 변수에는 암시적으로 'any[]' 형식이 포함됩니다.
    }

    for(let i = start; i < limit; i++) {
        out.push(i);
    }
    return out;  // number[] 로 추론되기 때문에 에러 없음.
}
  • 암시적 any 타입은 함수 호출을 거쳐도 진화하지 않는다.
function makeSqure(start: number, limit: number) {
    const out = []; //❌ ''out' 변수는 형식을 확인할 수 없는 경우 일부 위치에서 암시적으로 'any[]' 형식입니다.
    range(start, limit).forEach(i => {
        out.push(i * i);
    })
    return out; 
}
  • 의도치 않은 타입이 섞여서 잘못 진화할 수 있기 때문에, (암시적 any 진화 방식보단) 명시적 타입 구문 사용이 더 좋은 설계다.
const result = [];  //any[]
result.push(1);
result   //number[]

//보다는 처음부터 명시
const result: number[] = [];

 

아이템 42 모르는 타입의 값에는 any 대신 unknown사용하기

요약

  • unknown은 any 대신 사용할 수 있는 안전한 타입이다. 어떠한 값이 있지만 그 타입을 알지 못하는 경우라면 unknown을 사용함으로써 타입 단언문이나 타입 체크를 사용하도록 강제할 수 있다. (any 일 때 타입 체크가 안 되는 문제점이 보완된다.)
  • unknown는 모든 타입의 상위 타입이고, never는 모든 타입의 하위 타입이다. unknown 타입은 모든 타입이 될 수 있으나 모든 타입은 unknown이 될 수 없다. 반대로 never 타입은 모든 타입이 될 수 없고 모든 타입은 never가 될 수 있다.
  • {} 타입은 null과 undefined를 제외한 모든 값을 포함한다.

1. 어떠한 값이 있지만 그 타입을 알지 못하는 경우라면 any 대신 unknown을 사용하자.

  • unknown을 사용함으로써 타입 단언문이나 타입 체크를 사용하도록 강제할 수 있다.
    (any일 때 타입 체크가 안 되는 문제점이 보완된다.)
  • unknown 은 unknown 인 채로 사용하면 오류가 발생한다.
interface Book {
    name: string,
    author: string
}

//❌ 반환값이 any 여서 타입 체크가 안되어 런타임 에러가 발생한다.
const parseYamL = (yaml: string): any => {};

const book = parseYamL(`name: Jane 
                        author: Char`);
                        
//parseYamL를 호출한 곳에서 타입 선언이나 타입 단언을 하지 않으면 book이 any가 된다.
//따라서 아래 코드는 타입 에러도 나지 않고 런타임에서 에러가 난다.
console.log(book.title);
book();

//✅ 대신 unknown을 쓰자.
const safetyParseYamL = (text: string): unknown => ({});

const book2 = safetyParseYamL(`name: Jane 
                                author: Char`);
//unknown은 타입에러가 난다! 런타임 단계가 아니라 컴파일 단계에서 에러 확인 가능하다.

console.log(book2.title); // 'book2'은(는) 'unknown' 형식입니다.
book2(); // 'book2'은(는) 'unknown' 형식입니다.
  • 이중 단언문에서도 any 대신 unknown 을 사용할 수도 있다.
declare const foo: Foo;

let barAny = foo as any as Bar;
let barUnk = foo as unknown as Bar;

 
2. unknown는 모든 타입의 상위 타입이고, never는 모든 타입의 하위 타입이다.

  • 타입의 값을 집합으로 생각하기(아이템7)를 참고해보자.
  • any 타입은 모든 타입에 할당 될 수도 있고, 모든 타입이 any에 할당 될 수 있으니, 타입 시스템의 열외로 본다. 하지만 unknown은 타입 시스템에 부합한다.
  • any를 제외한 일반적인 타입은 타입 좁히기(더 작은 집합 되기)만 가능하다. 따라서 unknown 타입은 모든 타입이 될 수 있으나 모든 타입은 unknown이 될 수 없다. 반대로 never 타입은 모든 타입이 될 수 없고 모든 타입은 never가 될 수 있다.
let num: number;

function somethingDo(): unknown {
    return;
}

num = somethingDo(); //❌ 'unknown' 형식은 'number' 형식에 할당할 수 없습니다.

function hello(): never {
    throw new Error("xxx");
}

num = hello();  //✅

let imNever =  hello();
imNever = 12;  //❌ 'number' 형식은 'never' 형식에 할당할 수 없습니다

 
3. {} 타입은 null과 undefined를 제외한 모든 값을 포함한다.

  • unknown 타입이 도입되기 이전에는 {}가 일반적으로 사용되었다. 최근에는 잘 사용하지 않는다.
  • 정말로 null과 undefined가 불가능하다고 판단되는 경우에만 {}를 대신 사용해야 한다.
  • object 타입은 모든 비기본형(non-primitive) 타입으로 이루어진다. ex) 객체, 배열

 

아이템 43 몽키 패치보다는 안전한 타입을 사용하기

요약

  • 몽키패치란, 원래 소스코드를 변경하지 않고 실행 시 코드 기본 동작을 추가, 변경 또는 억제하는 기술을 의미한다. 자바스크립트에 있어서는 프로토타입에 특정 메소드 추가 한다거나 document 객체에 전역 변수를 삽입하는 등이 있다.
  • 전역 변수나 DOM에 데이터를 저장하는 것보다 데이터를 분리하여 사용해야 한다.
  • 어쩔수 없이 내장 타입에 데이터를 저장해야 하는 경우, 안전한 타입 접근법 중 하나인 보강이나 사용자 정의 인터페이스로 단언를 사용해야 한다
//보강
interface Document {
    monkey: string;
}

document.monkey = 'Tamarin';   //정상

//사용자 정의 인터페이스로 단언
interface MonkeyDocument extends Document {
    monkey: string;
}

(document as MonkeyDocument).monkey = 'Tamarin';  //정상

 

아이템 44 타입 커버리지를 추적하여 타입 안전성 유지하기

요약

  • noImplicitAny로 암묵적인 any타입 사용을 금지 시켜도, 명식적 any 또는 서드파티 타입 선언(@types)을 통해 any 타입은 코드 내에 여전히 존재할 수 있다는 점을 주의하자.
  • 작성한 프로그램 타입을 추적해 any 사용을 줄여나가자. npx type-coverage 를 사용해 프로젝트 심벌중 any 가 아닌 타입의 퍼센트를 확인할 수 있다. npx type-coverage --detail 를 사용해 any 타입이 있는 곳을 모두 출력할 수 있다.
$ npx type-coverage
// 9985 / 10117 98.69%

 

6장 타입 선언과 @types


아이템 45 devDependencies 에 typescript와 @types 추가하기

 
요약

  • typescript를 시스템 레벨(npm install -g typescript)로 설치하면 안 된다.
    typescript를 프로젝트의 devDependencies 에 포함시키고 팀원 모두가 동일한 버전을 사용하도록 해야 한다.
  • @types 의존성은 dependencies가 아니라 devDependencies에 포함시켜야 한다.
    런타임에 @types 가 필요한 경우라면 별도의 작업이 필요할 수 있다.

예시

//package.json
{
  ...
  "dependencies": {
    "lodash": "^4.17.21"
  },
  "devDependencies": {
    "typescript": "^5.0.2",
    "@types/lodash": "^4.14.192",
    "@types/jest": "^29.5.0",
    "jest": "^29.5.0"
  }
}
      • dependencies
        • 현재 프로젝트를 실행하는 필수적인 라이브러리들이 포함
        • 프로젝트를 npm 공개하여 다른 사용자가 해당 프로젝트를 설치한다면, dependencies 들어 있는 라이브러리도 함께 설치될 것
    • devDependencies
      • 현재 프로젝트를 개발하고 테스트하는 데 사용되지만, 런타임에는 필요 없는 라이브러리들이 포함됨

모든 타입스크립트 프로젝트에서 공통적으로 고려해야 의존성 2가지
1. 타입스크립트 자체 의존성을 고려해야 한다.

  • 시스템 레벨로 설치할 수 있지만, 팀원들 모두 동일한 버전을 설치한다고 보장할 수 없고, 프로젝트를 셋업할 때 별도의 단계가 추가됨
  • 따라서 타입스크립트를 시스템 레벨로 설치하기보다는 devDependencies에 넣는 것이 좋다.
    devDependencies에 포함되어 있다면, npm install을 실행할 때 팀원들 모두 항상 정확한 버전의 타입스크립트를 설치할 수 있습니다.

2. 타입 의존성(@types)을 고려해야 한다.

DefinitelyTyped
타입스크립트 커뮤니티에서 유지보수하고 있는 자바스크립트 라이브러리의 타입을 정의한 모음

사용하려는 라이브러리에 타입 선언이 포함되어 있지 않더라도, DefinitelyTyped에서 타입을 얻을 수 있다.
-> 원본 라이브러리 자체가 dependencies에 있더라도 ©types 의존성은 dev Dependencies에 있어야 합니다.
 
예시. 리액트의 타입 선언과 리액트를 의존성에 추가하는 명령어

$ npm install react
$ npm install --save-dev @types/react

그러면 다음과 같은 package.json 파일이 생성됨

{
    "devDependencies": {
        "@types/react": "~L6.8.19",
        "typescript": "3.5.3"
    },
    "dependencies": {
    	"react": "16.8.6"
    }
}

 

아이템 46 타입 선언과 관련된 가지 버전 이해하기

요약

  • 타입 선언과 관련된 세가지 버전이 있다. 라이브러리 버전, @types 버전, typescript 버전이다. 이 세가지의 버전에 따라 타입스크립트 생태계에선 더욱 의존성이 관리가 복잡하게 되었다.
  • 라이브러리를 업데이트하는 경우, 해당 @types 역시 업데이트를 하자.
  • 라이브러리를 만들때 타입 선언을 관리하는 방법은 두가지가 있다.
    • 방법1) 타입 선언을 DefinitelyTyped에 공개(= @types)해 라이브러리와 따로 두기
    • 방법2) 타입 선언을 라이브러리에 포함시키기
    • 공식적인 권장사항은 타입스크립트로 작성된 라이브러리인 경우만 타입 선언을 자체적으로 포함하는 것이다. 그 외의 경우(자바스크립트로 작성된 라이브러리)라면 타입을 DefinitelyTyped에 공개해 따로 관리하는게 좋다.

실제 라이브러리와 타입 정보의 버전이 별도로 관리되는 방식의 문제점

// 특정 라이브러리 - dependencies로 설치
$ npm install react
+ react@16.8.6

// 타입 정보 - devDependencies
$ npm install —save-dev @types/react
+ @types/react@16.8.19

-> 메이저 버전과 마이너 버전(16.8) 일치하지만 패치 버전(.6 .19) 일치하지 않다.
 
1. 라이브러리를 업데이트했지만 실수로 타입 선언은 업데이트하지 않는 경우
라이브러리 업데이트와 관련된 새로운 기능을 사용하려 할 때마다 타입 오류가 발생하게 됨
 
2. 라이브러리보다 타입 선언의 버전이 최신인 경우
타입 정보 없이 라이브러리를 사용해 오다가 타입 선언을 설치하려고 할 때 발생하게 됨
-> 타입 체커는 최신 API를 기준으로 코드를 검사하게 되지만 런타임에 실제로 쓰이는 것은 과거 버전
 
3. 프로젝트에서 사용하는 타입스크립트 버전보다 라이브러 리에서 필요로 하는 타입스크립트 버전이 최신인 경우
-> 프로젝트의 타입스크립트 버전을 올리거나 or
라이브러리 타입 선언의 버전을 원래대로 내리거나 or 
declare module 선언으로 라이브러리의 타입 정보를 없애 버리기

4. @types 의존성이 중복될 수 있음

 

아이템 47 공개 API 등장하는 모든 타입을 익스포트하기

요약

  • 공개 메서드에 등장한 어떤 형태의 타입이든 익스포트 하자.
    어차피 라이브러리 사용자가 추출할 수 있으므로 익스포트 하기 쉽게 만드는 것이 좋다. (어차피 숨기는게 거의 불가능하다.)

예시

interface SecretName {
    first: string;
    last: string;
}
interface SecretSanta {
    name: SecretName;
    gift: string;
}

export const getGift = (name: SecretName, gift: string): SecretSanta => {
    //...
};

// 타입 추출하기
type MySanta = ReturnType<typeof getGift>;     //SecretSanta
type MyName = Parameters<typeof getGift>[0];   //SecretName

 
 
 
출처: https://www.dgmunit1.com/blog/typescript/item45_52

아이템 34 부정확한 타입보다는 미완성 타입을 사용하기

타입이 구체적일수록 버그를 더 많이 잡고 타입스크립트가 제공하는 도구를 활용할 수 있게 된다.

하지만 부정확한 타입 설계는 없는 것보다 못하다

그리고 타입정보를 구체적으로 만들 수록 오류 메시지와 자동 완성 기능에 주의를 기울여야 한다.

 

예시 1. 타입을 구체적으로 개선한 코드

interface Point {
    type: 'Point';
    coordinates: number[];
}
interface Linestring {
    type: 'LineString';
    coordinates: number[][];
}
interface Polygon {
    type: 'Polygon';
    coordinates: number[][][];
}
type Geometry = Point | LineString | Polygon;

 

-> 좌표로 쓰이는 number[]가 추상적

여기서 number[] 경도와 위도를 나타내므로 튜플 타입으로 선언하는게 낫다.

type GeoPosition = [number, number];
interface Point {
    type: 'Point';
    coordinates: GeoPosition;
}

 

타입을 구체적으로 개선했기 때문에 더 나은 코드가 됨

 

예시 2. JSON으로 정의된 Lisp와 비슷한 언어의 타입 선언

Lisp
리스프(Lisp, LISP) 혹은 리습은 프로그래밍 언어의 하나로, 오랜 역사와 독특하게 괄호를 사용하는 문법으로 유명하다.
Lisp은 “LISt Processing”(리스트 프로세싱)의 줄임말.
type Expression4 = number | string | CallExpression;

type CallExpression = MathCall | CaseCall | RGBCall;

interface MathCall {
  0: '+' | '-' | '/' | '*' | '>' | '<';
  1: Expression4;
  2: Expression4;
  length: 3;
}

interface CaseCall {
  0: 'case';
  1: Expression4;
  2: Expression4;
  3: Expression4;
  length: 4 | 6 | 8 | 10 | 12 | 14 | 16 // etc.
}

interface RGBCall {
  0: 'rgb';
  1: Expression4;
  2: Expression4;
  3: Expression4;
  length: 4;
}

const tests: Expression4[] = [
  10,
  "red",
  true,
// ~~~ Type 'true' is not assignable to type 'Expression4'
  ["+", 10, 5],
  ["case", [">", 20, 10], "red", "blue", "green"],
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//  Type '["case", [">", ...], ...]' is not assignable to type 'string'
  ["**", 2, 31],
// ~~~~~~~~~~~~ Type '["**", number, number]' is not assignable to type 'string
  ["rgb", 255, 128, 64],
  ["rgb", 255, 128, 64, 73]
// ~~~~~~~~~~~~~~~~~~~~~~~~ Type '["rgb", number, number, number, number]'
//                          is not assignable to type 'string'
];
 const okExpressions: Expression4[] = [
   ['-', 12],
// ~~~~~~~~~ Type '["-", number]' is not assignable to type 'string'
   ['+', 1, 2, 3],
// ~~~~~~~~~~~~~~ Type '["+", number, ...]' is not assignable to type 'string'
   ['*', 2, 3, 4],
// ~~~~~~~~~~~~~~ Type '["*", number, ...]' is not assignable to type 'string'
 ];

CaseCall 타입이 사용된 요소에서는 배열의 길이가 초과하여 오류가 발생하였는데,
오류로 표현되어야 할
green이 아닌 MathCall 타입의 [”>”, 20, 10] 요소가 오류로 지목되었다.

 

Good

코드를 더 정밀하게 만들려던 시도가 너무 과했고 그로 인해 코드가 오히려 더 부정확해졌다.

이렇게 부정확함을 바로잡는 방법을 쓰는 대신, 테스트 세트를 추가하여 놓친 부분이 없는지 확인하는 것이 나은 방법일 수 있다.


아이템 35 데이터가 아닌, API와 명세를 보고 타입 만들기

여기서 핵심은 예시 데이터가 아니라 명세를 참고해 타입을 생성한다는 것

예시 데이터를 참고해 타입을 생성하면 눈앞에 있는 데이터들만 고려하게 되므로 예기치 않은 곳에서 오류가 발생할 수 있으나

명세를 이용해 타입을 생성하면 타입스크립트는 사용자가 실수를 줄일 수 있게 도와준다.

 

예시. GitHub 저장소에서 오픈 소스 라이선스를 조회하는 쿼리

GraphQL API는 타입스크립트와 비슷한 타입 시스템을 사용하여, 가능한 모든 쿼리와 인터페이스를 명세하는 스키마로 이루어진다.

우리는 이러한 인터페이스를 사용해서 특정 필드를 요청하는 쿼리를 작성한다.

 

GraphQL의 장점은 특정 쿼리에 대해 타입스크립트 타입을 생성할 수 있다는 것이다.

GeoJSON 예제와 마찬가지로 GraphQL을 사용한 방법도 타입에 null이 가능한지 여부를 정확하게 모델링할 수 있다.

query getLicense($owner: String!, $name:String!) {
  repository(owner:$owner, name:$name) {
    description
    licenseInfo {
      spdxId
      name
    }
  }
}

 

$owner와 $name은 타입이 정의된 GraphQL의 변수이다.

String은 GraphQL의 타입으로 타입스크립트의 string에 대응

타입 뒤의 ’!’는 null이 아님을 명시

 

Apollo는 GraphQL 쿼리를 타입스크립트 타입으로 변환해 주는 도구 중 하나

쿼리에서 타입을 생성하려면 GraphQL 스키마가 필요하다.

 

Apollo는 api.github.com/graphql로 부터 스키마를 얻는다.

$ apollo client:codegen \
    —endpoint https: //api. git hub. com/graphql \
    —includes license.graphql \
    --target typescript

 

실행결과

export interface getLicense_repository_licenseInfo {
  __typename: "License";
  /** Short identifier specified by <https://spdx.org/licenses> */
  spdxId: string | null;
  /** The license full name specified by <https://spdx.org/licenses> */
  name: string;
}

export interface getLicense_repository {
  __typename: "Repository";
  /** The description of the repository. */
  description: string | null;
  /** The license associated with the repository */
  licenseInfo: getLicense_repository_licenseInfo | null;
}

export interface getLicense {
  /** Lookup a given repository by the owner and repository name. */
  repository: getLicense_repository | null;
}

export interface getLicenseVariables {
  owner: string;
  name: string;
}

 

주목할 만한 점은 다음과 같다.

  • 쿼리 매개변수(getLicenseVariables)와 응답(getLicense) 모두 인터페이스가 생성되었다.
  • null 가능 여부는 스키마로부터 응답 인터페이스로 변환되었다.
  • 편집기에 확인할 수 있도록 주석은 JSDoc으로 변환되었다(아이템 48), 이 주석들은 GraphQL 스키마로부터 생성되었다.

=> 자동으로 생성된 타입 정보는 API를 정확히 사용할 수 있도록 도와준다.

쿼리가 바뀌면 타입도 자동으로 바뀌며, 스키마가 바뀌면 타입도 자동으로 바뀝니다.


아이템 36 해당 분야의 용어로 타입 이름 짓기

 

엄선된 타입, 속성, 변수의 이름은 의도를 명학히 하고 코드와 타입의 추상화 수준을 높여 준다.

잘못 선택한 타입 이름은 코드의 의도를 왜곡하고 잘못된 개념을 심어주게 된다.

 

코드로 표현하고자 하는 모든 분야에는 주제를 설명하기 위한 전문 용어들이 있다.

이런 용어들을 사용하면 소통에 유리하고 타입의 명확성을 올릴 수 있다.

 

주의할 점 3가지

  • 동일한 의미를 나타낼 때는 같은 용어를 사용한다.
  • data, info, thing, item, object, entity 같은 모호하고 의미없는 이름은 피한다.

만약 entity라는 용어가 해당 분야에서 특별한 의미를 가진다면 괜찮다.

  • 이름을 지을 때는 포함된 내용이나 계산 방식이 아니라 데이터 자체가 무엇인지를 고려해야 한다.
예를 들어, INodeList 보다는 Directory가 더 의미 있는 이름.
Directory는 구현의 측면이 아닌 개념적인 측면에서 보는 것

 

 

예시. 동물들의 데이터베이스를 구축한다고 가정해 보자

Bad

  • name: 매우 일반적인 용어, 동물의 학명인지 일반적인 명칭인지 알 수 없음
  • endangered: 멸종 위기를 표현하기 위해 boolean 타입을 사용한 것이 맞지 않음
  • habitat: 너무 넓은 범위의 string 타입이며, 서식지를 뜻하는 지도 불분명
interface Animal {
  name: string;
  endangered: boolean;
  habitat: string;
}

const leopard: Animal = {
  name: 'Snow Leopard',
  endangered: false,
  habitat: 'tundra',
};

Good

  • name을 commonName, genus, species 등 더 구체적인 용어로 대체
  • endangered를 status로 바꾸고, 동물 보호 등급에 대한 표준 체계를 사용하였다.
  • habitat을 climates으로 바꾸고 쾨펜 기후 분류를 사용하였다.
interface Animal {
  commonName: string;
  genus: string;
  species: string;
  status: ConservationStatus;
  climates: KoppenClimate[];
}

type ConservationStatus = 'EX' | 'EW' | 'CR' | 'EN' | 'VU' | 'NT' | 'LC';
type KoppenClimate = |
  'Af' | 'Am' | 'As' | 'Aw' |
  'BSh' | 'BSk' | 'BWh' | 'BWk' |
  'Cfa' | 'Cfb' | 'Cfc' | 'Csa' | 'Csb' | 'Csc' | 'Cwa' | 'Cwb' | 'Cwc' |
  'Dfa' | 'Dfb' | 'Dfc' | 'Dfd' |
  'Dsa' | 'Dsb' | 'Dsc' | 'Dwa' | 'Dwb' | 'Dwc' | 'Dwd' |
  'EF' | 'ET';

const snowLeopard: Animal = {
  commonName: 'Snow Leopard',
  genus: 'Panthera',
  species: 'Uncia',
  status: 'VU',  // vulnerable
  climates: ['ET', 'EF', 'Dfd'],  // alpine or subalpine
};

아이템 37 공식 명칭에는 상표를 붙이기

타입스크립트는 구조적 타이핑을 사용하기 때문에, 값을 세밀하게 구분하지 못하는 경우가 있다.

값을 구분하기 위해 공식 명칭이 필요하다면 상표를 붙이는 것을 고려해야 한다.

 

상표 기법은 타입 시스템에서 동작하지만 런타임에서 상표를 검사하는 것과 동일한 효과를 얻을 수 있다.

타입 시스템이기 때문에 런타임 오버헤드를 없앨 수 있고 추가 속성을 붙일 수 없는 string이나 number 같은 내장 타입도 상표화 할 수 있다.

구조적 타이핑
: 실제 구조와 정의에 의해 결정되는 타입 시스템

 

예시 1. 벡터의 길이를 계산하는 함수

interface Vector2D {
  x: number;
  y: number;
}

function calculateNorm(p: Vector2D) {
  return Math.sqrt(p.x * p.x + p.y * p.y);
}

calculateNorm({x: 3, y: 4});  // OK, result is 5

const vec3D = {x: 3, y: 4, z: 1};
calculateNorm(vec3D);  // OK! result is also 5

 

Good

calculateNorm 함수가 3차원 벡터를 허용하지 않게 하려면 공식 명칭(nominal-typing)을 사용하면 된다.

공식 명칭을 사용하는 것은, 타입이 아니라 값의 관점에서 Vector2D라고 말하는 것

interface Vector2D {
  _brand: '2d';
  x: number;
  y: number;
}

function vec2D(x: number, y: number): Vector2D {
  return {x, y, _brand: '2d'};
}

function calculateNorm(p: Vector2D) {
  return Math.sqrt(p.x * p.x + p.y * p.y);  // Same as before
}

calculateNorm(vec2D(3, 4)); // OK, returns 5

const vec3D = {x: 3, y: 4, z: 1};
calculateNorm(vec3D);
           // ~~~~~ Property '_brand' is missing in type...

 

그러나 vec3D 값에 _brand: '2d'라고 추가하는 같은 악의적인 용을 막을 수는 없다.

다만 단순한 실수를 방지하기에는 충분하다.

 

예시 2. 절대 경로를 사용해 파일 시스템에 접근하는 함수

Good

string 타입이면서 _brand 속성을 가지는 객체를 만들 수는 없다.

타입 AbsolutePath는 온전히 타입 시스템의 영역

 

만약 path 값이 절대 경로와 상대 경로 둘 다 될 수 있다면, 타입을 정제해 주는 타입 가드를 사용해서 오류를 방지할 수 있다.

type AbsolutePath = string & {_brand: 'abs'};

function listAbsolutePath(path: AbsolutePath) {
  // ...
}

function isAbsolutePath(path: string): path is AbsolutePath {
  return path.startsWith('/');
}

function f(path: string) {
  if (isAbsolutePath(path)) {
    listAbsolutePath(path);
  }
  listAbsolutePath(path);
                // ~~~~ Argument of type 'string' is not assignable
                //      to parameter of type 'AbsolutePath'
}

 

예시 3. number 타입에 상표 붙이기

type Meters = number & {_brand: 'meters'};
type Seconds = number & {_brand: 'seconds'};
const meters = (m: number) => m as Meters;
const seconds = (s: number) => s as Seconds;
const oneKm = meters(1000); // 타입이 Meters
const oneMin = seconds (60); // 타입이 Seconds

 

하지만, number 타입에 상표를 붙여도 산술 연산 후에는 상표가 없어지기 때문에 실제로 사용하기에는 무리가 있다. 

그러나 숫자의 단위를 문서화하는 괜찮은 방법일 수 있다.

const tenKm = oneKm * 10; // 타입이 number
const v = oneKm / oneMin; // 타입이 number

 


5장 any 다루기

아이템 38 any 타입은 가능한 한 좁은 범위에서만 사용하기

요약

  • 의도치 않은 타입 안전성의 손실을 피하기 위해서 any의 사용 범위를 최소한으로 좁혀야 합니다.
  • 함수의 반환 타입이 any인 경우 타입 안정성이 나빠집니다. 따라서 any 타입을 반환하면 절대 안 됩니다.
  • 강제로 타입 오류를 제거하려면 any 대신 @ts-ignore 사용하는 것이 좋습니다.

예시 1. 함수 내에서 사용된 any

function f1() {
  const x: any = expressionReturningFoo(); // 이렇게 하지 맙시다.
  processBar(x);
}
function f2() {
  const x = expressionReturningFoo();
  processBar(x as any); // 이게 낫습니다.
}

f1 함수: x를 any 타입으로 지정
f2 함수: x가 사용되는 곳에 as any 단언문 사용

 

f2 함수가 권장되는 이유:

any 타입이 processBar 함수의 매개변수에서만 사용이 되기 때문에, 다른 코드에는 영향이 미치지 않음 !

반면에, f1 함수에서는 마지막까지 x가 any 타입이 유지된다.

 

f1 함수가 x를 반환한다면?

function f1() {
  const x: any = expressionReturningFoo();
  processBar(x);
return x;

function g() {
  const foo = f1(); // 타입이 any
   foo.fooMethod(); // 이 함수 호출은 체크되지 않습니다!
}

 

=> 함수 호출이 체크되지 않기 때문에 이렇게 사용하면 안됨

이렇게 any 타입인 x를 반환하는 함수를 사용하면 foo 타입까지 영향을 미치기 때문에, 프로젝트 전반에 전염병처럼 퍼지게 된다.

 

f2 함수 처럼 사용한다면 any 타입이 함수 바깥으로 영향을 미치지 않는다.

function f1() {
    const x = expressionReturningFoo();
    // @ts-ignore
    processBar(x);
    return x;
}

@ts-ignore 를 사용하여 오류를 무시할 수 있지만, 근본적인 원인을 해결한 것은 아니기 때문에 다른 곳에 더 큰 문제가 발생할 수 있음

 

예시 2. 객체와 관련한 any 사용법

const config: Config = {
    a: 1,
    b: 2,
    c: {
        key: value
        // ~
        'foo' 속성이 'Foo' 타입에 필요하지만 'Bar' 타입에는 없습니다.
    }
};

 

단순히 생각하면 config 객체 전체를 as any로 선언해서 오류를 제거할 수 있다.

const config: Config = {
  a: 1,
  b: 2,
  c: {
  key: value
  }
} as any; // 이렇게 하지 맙시다!

 

하지만, 객체 전체를 any로 단언하면 다른 속성들 (a, b) 역시 타입 체크가 되지 않는다 !
그러므로 아래처럼 최소한의 범위에만 any를 사용하는 것이 좋다.

const config: Config = {
  a: 1,
  b: 2, // 이 속성은 여전히 체크됩니다.
  c: {
  key: value as any
  }
};

아이템 39 any를 구체적으로 변형해서 사용하기

요약

  • any를 사용할 때는 정말로 모든 값이 허용되어야만 하는지 면밀히 검토해야 한다.
  • any보다 더 정확하게 모델링 할 수 있도록 any[] 또는 {[id: string]: any} 또는 () => any처럼 구체적인 형태를 사용해야한다.

1. any를 사용할 때는 정말로 모든 값이 허용되어야만 하는지 면밀히 검토해야 한다.

  • any 타입은 모든 숫자, 문자열, 배열, 객체, 정규식, 함수, 클래스, DOM 엘레먼트, null, undefined 까지 포함한다.
  • 일반적인 상황에서 any 보다 더 구체적으로 표현할 수 있는 타입이 존재할 가능성이 높다.
const numArgsBad = (...args: any) => args.length; //❌ return 타입이 any
const numArgsGood = (...args: any[]) => args.length; //✅ return 타입이 number

/**
후자가 더 좋은 이유
1. args.length 타입이 체크 됨
2. 함수 반환 타입이 number 로 추론 됨
3. 함수 호출 시, 매개변수가 배열인지 체크 됨
*/

 

2. any보다 더 정확하게 모델링 할 수 있도록 any[] 또는 {[id: string]: any} 또는 () => any처럼 구체적인 형태를 사용해야한다.
(그냥 any 보다는 조금이라도 더 구체화시키려 노력하자.)

 


아이템 40. 함수 안으로 타입 단언문 감추기

요약

  • 타입 단언문은 일반적으로 타입을 위험하게 만들지만 상황에 따라 필요하기도 하고 현실적인 해결책이 되기도 한다. 불가피하게 사용해야 한다면, 정확한 정의를 가지는 함수 안으로 숨기도록 한다.
    • 가능한 안전한 타입으로 구현하는 것이 이상적이나, 불필요한 예외 상황까지 고려해 가며 타입 정보를 힘들게 구성할 필요는 없다.
    • 함수 내부에서는 타입 단언 을 사용하고, 함수 외부로 드러나는 타입 정의를 명시하는 정도가 적절할 수 있다.
//예제 - 함수 캐싱하는 함수: 함수가 자신의 마지막 호출을 캐시(기억)하도록 만들어주는 함수
declare function shallowEqual(a: any, b: any): boolean;
function cacheLast<T extends Function>(fn: T): T {
    let lastArgs: any[] | null = null;
    let lastResult: any;

    return function(...args: any[]) {
        if (!lastArgs || !shallowEqual(lastArgs, args)) {
            lastResult = fn(...args);
            lastArgs = args;
        }
        return lastResult;
    } as unknown as T;
}

//원본 함수 타입 T와 리턴하는 함수가 어떤 관련이 있는지 모르기 때문에 에러 발생!
//그려나 우리는 두 함수가 '같은 매개변수를 주면 같은 값을 반환하는' 함수여서 
//동일하게 취급해도 문제 없다는 것을 알기때문에 단언문 사용해도 괜찮다.

 

리액트 환경의 무한 스크롤

무한 스크롤은 페이지 하단 영역까지 스크롤될 경우 다른 컨텐츠를 새롭게 로딩해 페이지에 추가되는 방식이다.

react-intersection-observer 라이브러리를 사용해서 구현해보았다.

 

- 라이브러리 설치

$ npm install react-intersection-observer --save

 

- useInView hook

  const { ref: observerRef, inView } = useInView()

 

=> 관찰하는 객체 (observer) 하나를 ref로 설정한 후 해당하는 객체가 화면에 보이면 특정 코드를 실행시킬 수 있다.

https://github.com/thebuilder/react-intersection-observer

 

GitHub - thebuilder/react-intersection-observer: React implementation of the Intersection Observer API to tell you when an eleme

React implementation of the Intersection Observer API to tell you when an element enters or leaves the viewport. - thebuilder/react-intersection-observer

github.com

 

- 전체 코드

import { useInView } from 'react-intersection-observer'

const Bookmark = () => {
	const { ref: observerRef, inView } = useInView()

	React.useEffect(() => {
        if (inView) {
          const totalPage = queryBookmarks.data.totalPage
          if (totalPage != null && bookmarkPage < totalPage) {
          	// 뷰포트에 들어오면 count + 1 해주기
            setBookmarkPage((prev) => prev + 1)
          }
        }
    }, [inView])


    return (
    	<div>
    		<div>
                {items.map((item) => (
                    <BookmarkItem 
                        title={bookmark.title}
                        ...
                    />
                )}
            </div>
         <div ref={observerRef} />
       </div>
    )
 }

 


문제 상황

useInView의 observerRef가 뷰포트에 들어올 때마다 inView 상태가 갱신되는 방식으로 구현을 했는데, 

페이지가 다시 로딩하는 문제가 일어났다.

이유는 무한 스크롤이나 하단에 위치한 요소가 여러 번 감지되면 이런 문제가 발생하는데, observerRef가 계속해서 inView 상태를 변경하기 때문이다.

 

해결법

useInView의 triggerOnce 옵션을 활용하여 한 번만 실행할 수 있도록 하자.

triggerOnce: true를 설정하면, 한 번 감지된 이후에는 inView 상태가 유지되어, 반복적으로 호출되지 않도록 제어할 수 있다.

const { ref: observerRef, inView } = useInView({
  triggerOnce: true, // 추가
});

return (
	<div>
    	{inView && ( // 추가
        	<div>
                <BookmarkItem
                 ...
                 />
                <div ref={observerRef} />
            </div>
         }
    </div>
)

 

 

추가로 inView가 True 일 때만 해당 아이템이 보이도록 구현한다.

Selenium 자동화 코드를 작성하면서 텍스트 입력된 내용을 삭제하는 코드를 자주 쓰게 되는데,

처음 알게된 내용을 공유하려고 한다.

 

네이버 쇼핑에서 여러 개의 키워드를 검색해서 내용을 가져오는 웹 크롤링을 하다가

아래 코드에서 이슈가 발생했다.

 

이슈

keywords = ['라면', '바구니', '커피']
for idx, keyword in enumerate(keywords):
    input = wait.until(EC.element_to_be_clickable((By.XPATH, '//*[@id="gnb-gnb"]/div[2]/div/div[2]/div/div[2]/form/div[1]/div/input')))
    input.send_keys(keyword)

 

키워드 배열을 돌면서 input 안에 keyword를 넣게 되는데, 

이상하게도 이전에 입력된 값이 다음 키워드에도 합쳐져서 입력이 됐다.

 

예를 들어 '라면'을 검색한 이후에 '바구니'를 검색하면,

'라면바구니' 라고 검색됐다.

 

입력된 input을 all clear하는 방법은 없을까 구글링을 하다가,

결국 찾아냈다!

 

해결법

input.send_keys(Keys.LEFT_SHIFT, Keys.HOME, Keys.BACKSPACE)

 

Shift 키와 Home 키를 사용하여 입력된 텍스트 전체 선택을 하고, Backspace로 삭제하는 개념이다.

 

send_keys가 아닌 all_clear 이런 메서드라도 있는 줄 알았더만 ..

속절없이 지나가는 시간들을 붙잡고 싶어 .. 쓰기 시작한 나의 첫 회고
항상 회고라는 것을 해보고 싶었지만, 바쁘다는 핑계로 미뤄왔었다.
하지만 좋은 기운들을 마주하고 잊지 않기 위해서, 누군가에게 전달받은 동기부여들을 잊지 않기 위해 기록해본다 🙌
 
이번 2024년은 정말 많은 것들이 변했다.
대운이 들어오기 전 오래 유지되었던 인간관계가 변하고, 새로운 사람들을 많이 만난다던데, 지금이 그 시기인듯하다.
가장 많이 변했다면 변한 2024년, 여러 사건들을 마주하며 심적으로 많이 힘들었지만,
혼자만의 시간을 제일 많이 보내면서 단단해진 것 같다 😇


 
1분기 (1~3월) :
1. 호주 여행
운이 좋게 친구가 퇴사하고, 나는 방학을 맞아 길다면 긴 2주간의 호주 여행을 다녀왔다.
호주가서 호주인이 다 된 고등학교 친구를 만나고, 또 처음보는 외국인들과 같이 놀면서 정말 재밌었다.
휴식하고 싶다는 마음 가짐으로 찾아간 호주 여행에서 세상은 넓고, 무엇이든 할 수 있겠다라는 마음으로 돌아왔다.
호주 워홀을 30대 이전에 꼭 가겠다는 마음으로 .. 돌아왔는데, 막상 한국이 더 좋긴 하다. 돈만 있으면 살기 좋은 곳 나라 = 한국
 
여행 다녀온 뒤, 유튜브에 여행 브이로그 만들어서 올렸다.
물론 영상 1개만 편집하고 그 이후에는 흥미가 떨어져서 못올렸다는 소문이 ..
다시 오세아니아를 간다면 기필코 호주 브리즈번과 퍼스, 뉴질랜드를 다시 가리라 ..

 
 
2. 숙대 졸업
졸업할 수 있을까? 외치던 내가 드디어 졸업을 했다. 많은 친구들과 가족들이 축하해준 덕분에 외롭지 않게 졸업할 수 있었다 ㅎㅎ
입학하고 스터디, 카이스트 몰입캠프, 큐시즘, 시스템 종합 설계 수업을 통해 많은 친구들을 만나고, 또 같이 개발하면서 정말 많은 것들을 배우게 되었는데 ,, 어쩌다 보니 개발인생이 되었다 ㅎㅎ 신기한 모먼트
전공 수업보다는 교양 수업들이 더 기억에 남고 재미있었다.
그 중 졸업하기 전에 마지막으로 들었던 비판적 사고와 토론 수업이 가장 기억에 남는다.
학우들과 하나의 논제를 가지고 찬성, 반대 근거를 가져와 토론을 하게 되는데, 각자의 근거를 준비해서 토론하는 이 과정이 너무 즐거웠다.
매도 먼저 맞는 것이 낫다고 ,, 내가 갑, 을, 병 중 갑이 되어서 입론을 펼치면서 토론을 시작했는데,
발표라고는 치를 떠는 내가 많은 학우들이 보고 있는 앞에서 토론을 진행하면서, 떨지 않고 발언하는 게 꽤 멋졌다. 많이 성장했군 ,,
덕분에 A 학점을 취득했고 매우 뿌듯했던 경험이었다. 팀원을 잘만나서 더더 재미있었다 ㅎㅎ

 
 
2. 구직 준비하면서 토익, 토익스피킹 자격증 따기
삼성 넣기 위해 처음으로 토스를 준비해봤는데,, 제니쌤과 함께 3일을 보내고 과감없이 시험을 쳐버렸다.
물론 좋은 결과는 아니지만, 턱걸이로 지원해볼 수 있었다 하 하 ..
근데 삼성이 인재를 못알아봤네 (?)
 
3. 운동 시작
처음으로 PT를 끊고 운동을 시작하게 되었다. 주연이랑 2인 PT하면서 더 좋은 시너지를 낼 수 있었다.
자기 관리 끝판왕 주연이를 보면서 식습관도 바꿀 수 있었고, 덕분에 더 재밌게 운동할 수 있었다 ㅎㅎ
가장 잘한 일 중 하나가 운동이라는 생각이 드는데, 5개월동안 체지방률 -4%, 근육 +1kg 늘렸다.
어쩌다보니 웨이트가 취미가 되고 있는데, 여전히 유산소는 하기 싫다 ㅎㅎㅎ
 
4. 프리랜서로 근무
운이 좋게 학교 공고글에 올라온 프론트엔드 모집 공고글을 보고 호다닥 지원했는데, 서류만 보고 바로 일하게 되었다.
처음으로 Next.js로 웹사이트 개발도 해보고, 팀에서 팀장(?)처럼 팀원을 관리하며 주도적으로 개발해보았다.
덕분에 취준 생활 ,, 풍요롭게 할 수 있었다..


2분기 (4~6월) : 
1. 네이버 블로그 시작
가장 친한 친구와 네이버 블로그를 시작했다. 우리가 가장 익숙하고도 편안한 장소를 시작으로 후기를 남기고, 이웃들과 소통하면서
점점 늘어가는 이웃들을 보면서 뿌듯했다. 또 체험단도 열심히 다니면서 식비도 아끼고, 좋은 경험들을 할 수 있었다.
이후에 면접에서 최근에 한 일 중에 성과를 낸 경험이 있냐고 물어봤을 때 생각난게 네이버 블로그 ㅋㅋㅋㅋㅋ
태생이 N잡러일지도 모르겠다.. 블로거도 쉽지 않음 ,, ㅎㅎ
 
2. 요리 실력
운동을 하면서 자연스럽게 외식을 줄이고 몸에 좋은 음식을 먹기 위해 매일 같이 요리했다.
단호박, 오리고기, 닭가슴살 등의 식단을 통해서 나의 입맛이 변화시키고, 또 초코처돌이인 나는 코코아파우더도 구매해서 베이킹도 하기 시작. 물론 요즘은 식단을 열심히 안해서 베이킹은 안하지만,, 조만간 바프를 찍을 수도 있기 때문에 다시 시작 예정이다 ✨
밤호박 철이던 시절이 그립군 ,,

내가 만든 에그타르트 / 초코케이크 / 즐겨먹던 식단

 
 
3. 태국 치앙마이 여행
엄마랑 외할머니랑 처음으로 여행을 갔다. 항상 자유여행만 가다가 어른들과 패키지 여행을 가게 됐는데, 버스에 앉아만 있으면 되니 더할나위없이 편했다. 
외할머니랑 항상 명절에만 만나다가 이렇게 여행가니까 더욱 친해질 수 있었다 ㅎㅎ 할머니는 알고보니 .. 굉장히 까다로운 사람이었다..
할머니와 처음이자 마지막일지도 모르는 여행이여서 더더욱 재밌었다. 행복해하시는 모습들을 보니 나 역시 행복했다.
송크란 축제 때 방문했으면 더 재밌었을 것 같은데, 그래도 투어리스트가 태국 역사와 카렌족 같은 원주민에 대해 얘기해주셔서 재밌었다 ㅎㅎ
 
4. 홍콩 여행
아마도 이때가 리즈 시절이 아니였을까 생각한다. 가장 말랐고.. 가장 예뻤나 (?) 아무튼 찍히는 모든 사진들이 마음에 쏙 들었다.
홍콩에서 정말 여러 일들이 있었는데, 
 
- 처음으로 홍콩 공항에서 노숙해본 경험..
애매한 시간에 도착해서 숙소 값 아까워 해외 공항에서 노숙해본 사람 있나요 ?
인천 공항처럼 인프라가 좋으면 모르겠으나, 의자 3개에서 웅크리고 자야했던 우리 ,, 정말 대단하다는 생각이 든다.
이런 경험도 젊을 때나 해보지 언제 해보겠어 (?)
 
- 길에서 만난 한국인과의 동행
마카오에서 친구랑 같이 걸어가던 중 혼자 오신 어머님이 우리보고 한국인이냐며 말을 거셨다.
지도 앱이 없으셔서 이정표만 보고 걷는다면서 동행하는 것이 어떠냐고 하셨다. 우리는 그렇게 동행하게 되고, 
알고보니 한국에서 교수님이셨다. 덕분에 폴로 공장도 방문하게 되고, 마카오에서 맛있는 것도 많이 사주셨다..
이 때 많이 사주신 덕분에 우리는 가까스로 홍콩으로 돌아가는 크루즈를 탈 수 있게 되었는데,, 정말 천운이었다 🥹
교수님께서 많은 조언을 해주셨는데,,, 가장 기억에 남는 말은 젊을 때 많이 여행다니라면서 나이 들면 돈은 많아도 감흥이 없다고 하셨다.
 
홍콩의 날씨는 무진장 더웠는데, 습함이 Max였다. 머리카락이 이마에 붙어있는 수준 ㅎㅎㅎ
현지에서 먹던 에그타르트와 밀크티, 납짝 복숭아가 너무너무 그립다 !

 
 
5. 네이버 부스트캠프 지원
웹 풀스택 개발자로 지원해서 코딩테스트도 보고, 입과자로 선정돼서 열심히 참여했다. (물론 1주차만..)
매일매일 요구사항을 분석하고 코드를 짜면서 금요일에는 4명의 팀원들과 하나의 문제를 풀었다.
그룹 미션을 진행하면서 다양한 사람들을 만날 수 있었고, 고등학교 동창도 만날 수 있었다.. 당황
이때 너무 바빠서 1주차만 진행하고 2주차는 함께하지 못했다.
 


 
3분기 (7~9월) : 
회사에서 나의 가치를 인정받은 3달.
회사에 입사 이후 어떤 것을 얻었나?
 
1. 인재밀도 높은 사람들 만나기
해외 출신, 국내 탑 대학교 출신들과 만나면서 세상에는 정말 멋진 사람들이 많고, 더 넓은 사고를 통해 세상을 바라봐야한다는 것을 깨달았다.
또 나와 다르게 겸손한 사람들, 타인의 감정을 공감하고 이해하는 사람들을 보면서 정말 대단하고 멋지다는 생각이 들었다.
통근을 3시간 하면서 자연스레 예민해지고, 심적으로 여유가 없어지는 것을 깨닫고 있는 와중에,,
항상 밝은 얼굴로 인사해주는 사람들과 본인의 커리어와 방향성에 대해 항상 고민하고 성장하려는 사람들이 너무 멋지다.
그런 사람들을 만날 수 있어서 너무 즐거웠고, 또 나이가 비슷한 사람들과 함께 할 수 있어서 너무 행복했다.
 
2. 코드에 대한 두려움이 사라지다.
처음 사회생활을 시작하고 개발을 할 때에는 내가 짠 코드에 대해 100% 확신할 수 없었다.
얼렁뚱땅 개발해도 잘 돌아가니까 .. 
해결되지 않는 문제들이 생겨나면 '내가 이 문제를 해결할 수 있을까?' 라는 생각이 온 몸을 지배했고, 
그 압박감이 자연스레 디버깅하는 데 두려움을 안겨주었다.
하지만, 그동안의 기간동안 여러 프로젝트를 진행해보면서 개발자는 개발"만"하는 것이 아닌, 문제를 해결하는 사람이라는 것을 깨달았다. 
어떤 문제가 발생하면 WHY와 HOW를 생각하며 디버깅했고, 두려워하지 않으니 자연스럽게 문제가 해결되었다.
이런 생각들이 개발적인 성장으로 이어졌고, 나에 대한 자신감으로 돌아왔다.
안되는 것도 되게 만들자 그게 개발자가 할 일
 
3. 앱 서비스 출시
오랜만에 서비스를 출시했다. 기존에 하던 테스트앱 배포, 개발, QA, 출시까지 완벽한 플로우로 이뤄졌다.
우리 앱은 외주로부터 이어졌지만, 내가 입사한 이후 "내 정보" 기능까지 개발하여 배포할 수 있었다.
그 중 패키지명 이슈가 있었는데, 아무런 패키지명으로 배포했다가 호되게 당했다.
패키지명은 서버 도메인을 따라야하고, 출시 이후에는 바꿀 수 없다는 점이 기억에 남는다.
또한, 처음으로 안드로이드 난독화해서 앱 출시도 해보고, 커머스 도메인 지식을 늘릴 수 있는 엄청난 기회였다.
현재는 다른 기능도 개발 진행중인데, 추후에는 비즈니스 성과를 꼭 증명할 수 있길 바랄 뿐이다.
 
4. 독서모임 시작
평소에 책이라고는 자기계발 쪽만 읽던 내가 고전을 읽게 되었다. 벌써 4권 째 ..!!
달과 6펜스에서 풍족한 현실을 버리고 과감히 본인의 이상을 추구하는 .. 스트릭랜드가 기억에 남는다. 난 절대 못해
며칠 전 홍기한테 "너 달과 6펜스에 나오는 스트릭랜드 같아" 이랬더니 걔는 본인보다 더하다면서
"그 사람 가정도 버리지 않았나? 나는 그렇겐 못해" 라고 했다 ㅋㅋㅋ

 
 
5. 해외구매대행 시작
인생 처음으로 사업자등록, 통신판매업 신고를 하면서 많은 어려움이 있었지만,, N잡의 재미를 보고 있다.
하지만 사업은 어려운법.. 소비자의 마음을 얻기란 쉽지 않다 ㅎㅎ
25살에 나의 커리어 목표를 잡을 수 있었는데, 목표를 향해 달려가는 나 제법 멋지다.
그곳에는 혼자가 아닌 제일 친한 친구와 함께 동행할 수 있음에 너무 감사할 따름 🙏
 
6. 인간관계의 변화
어렸을 때 친했던 친구들을 오랜만에 만나면 안맞는다는 생각이 들 때가 많다.
살아오면서 바뀐 가치관과 성격들이 고착화돼서 서로를 이해하지 못하게 만든다.
떠나갈 사람은 자연스럽게 떠나고, 나를 지켜줄 인연은 계속 유지된다.
인생은 내 마음대로 흘러가지 않을 뿐더러 스스로 통제하려해도 마음대로 되지 않는 것이다.
그러니 관계는 흘러가는 대로 두는 것이 가장 베스트인 듯 하다 😁
 
8. 스스로의 변화
동네 이상으로 멀리나가는 것을 싫어하던 내가
통근 시간이 거진 도합 3시간으로 다져진 습관으로 .. 이제 1시간은 무리 없다 !
오히려 익숙한 공간에서 벗어나 새로운 공간에서 얻어지는 인사이트들이 삶을 더 풍요롭게 만든다.
역시 나는 뽀로로인가보다 .. 노는 게 제일 좋아
 


 
마무리
입사 이후 현실 생활에 집중하느라 정신없이 시간이 흘러버렸다.
몸이 2개는 아니지만, 하나에만 몰두하지말고 On/Off를 잘해서 여러 일들에 시간을 할애했으면 좋겠다.
이렇게 회고를 해보니 정말 열심히 살았구나 싶다. 올해 여행을 3번이나 갔다니 정말 만족스럽군 ㅎㅎㅎ
4분기에도 회고를 해야지 .. !
그리고 linkedin 하는 person 있으면 알려주세요.. 팔로우하게 .. >.<

아이템 1 ~ 5 정리본

아이템 1: 타입스크립트와 자바스크립트의 관계 이해하기

모든 자바스크립트는 타입스크립트지만, 모든 타입스크립트는 자바스크립트가 아니다.

let city = 'new york city';
console.log (city.toUppercase());
// 'toUppercase* 속성이 'string* 형식에 없습니다.
// 'toUpperCase1 을(를) 사용하시겠습니까?

city 변수가 문자열이라는 것을 알려주지 않아도 타입스크립트는 초깃값으로부터 타입을 추론한다.

타입 추론은 ts에서 중요한 부분 !!

 

타입 시스템의 목표

  • 런타임에 오류를 발생시킬 코드를 미리 찾아내는 것

하지만, 타입 체커가 모든 오류를 찾아내지는 않음

const states = [
    {name: 'Alabama'f capital: 'Montgomery'},
    {name: 'Alaska', capital: ’Juneau'},
    {name: 'Arizona', capital: 'Phoenix'},
];

for (const state of states) {
    console.log(state.capitol);
}

// undefined
// undefined
// undefined

for (const state of states) {
    console.log(state.capitol);
    // ----------- 'capitol' 속성이 ... 형식에 없습니다.
    // 'capital'을(를) 사용하시겠습니까?
}
  • 타입스크립트는 어느 쪽이 오타인지 판단하지 못한다.
  • 오류의 원인은 추측할 수 있겠지만 항상 정확하지는 않다.

⇒ 명시적으로 states를 선언하여 의도를 분명하게 하자.

 

interface 사용

interface State {
    name: string;
    capital: string;
}
const states: State[] = [
    {name: 'Alabama', capitol: 'Montgomery'}
                                        // ------------
    {name: 'Alaskar', capitol: 'Juneau'},
                                        // ------------
    {name: 'Arizona', capitol: 'Phoenix'},
                                        // ------------
    // 개체 리터럴은 알려진 속성만 지정할 수 있지만
    // 'State' 형식에 'capitol'이(가) 없습니다.
    // 'capital'을(를) 쓰려고 했습니까?
    // ...
];
for (const state of states) {
    console.log(state.capital);
}

의도를 분명하게 하여, 오류가 어디서 발생했는지 찾을 수 있음 !

 

아이템 2: 타입스크립트 설정 이해하기

command line 으로 설정

$ tsc —noImplicitAny program.ts

or

tsconfig.json 파일로 설정

{
    "compilerOptions": {
        "noImplicitAny": true
    }
}

위 설정을 통해 타입을 제어할 수 있다.

 

strictNullChecks

  • null과 undefined가 모든 타입에서 허용되는지 확인하는 설정
// strictNullChecks = true
const x: number = null; // 정상, null은 유효한 값

// strictNullChecks = false
const x: number = null; // 비정상, 'null' 형식은 'number' 형식에 할당할 수 없습니다.

 

아이템 3: 코드 생성(컴파일)과 타입이 관계없음을 이해하기

타입스크립트 컴파일러 역할 독립적으로 수행

  1. ts/js를 브라우저에서 동작할 수 있도록 구버전의 js로 트랜스파일 (번역 + 컴파일)
  2. 타입 오류 체크

따라서, ts가 js로 변환될 때 코드 내의 타입에는 영향을 주지 않는다.

또한, 자바스크립트의 실행 시점에도 타입은 영향을 미치지 않는다.

타입 오류가 있는 코드도 컴파일이 가능함
컴파일은 타입 체크와 독립적으로 동작하기 때문에, 타입 오류가 있는 코드도 컴파일 가능
cf. C나 자바의 경우 타입 체크와 컴파일이 동시에 이루어짐

오류가 있을 때 컴파일하지 않으려면 ..

  • tsconfig.json에 noEmitOnError를 설정하거나 빌드 도구에 적용한다.

런타임에는 타입 체크가 불가능하다

instanceof 체크는 런타임에 일어나지만, Rectangle은 타입이기 때문에 런타임 시점에서 아무것도 할 수 없다.

cf ) instanceof + Class명

 

런타임에 타입을 지정하기 위해서는 ?

  • 런타임에 타입 정보를 유지하는 방법

1. 속성이 존재하는지 체크한다.

2. 런타임에 접근 가능한 타입 정보를 명시적으로 저장하는 ‘태그’ 기법이 있다.

 

아이템 4: 구조적 타이핑에 익숙해지기

  • 자바스크립트는 덕 타이핑 (duck typing) 기반으로, 타입스크립트는 이를 모델링하기 위해 구조적 타이핑을 사용함

덕 타이핑

  • 타입을 미리 정하는게 아닌 실행이 되었을 때 해당 method를 확인하여 타입을 정함

구조적 타이핑

  • y가 최소한 x와 동일한 멤버를 가지고 있다면 x와 y는 호환된다.
type Person = {
    name: string
};

let person: Person

const employee = {
    name: 'Anecdote',
    job: 'Developer',
}

person = employee; // OK

 

employeePerson 타입에 필요한 name 속성을 가지고 있기 때문에 그 외의 속성이 있더라도 person의 값으로 할당할 수 있다.

 

타입스크립트는 다른 인터페이스여도 이해할 수 있을만큼 영리하다.

interface Vector2D {
    x: number;
    y: number;
}

function calculateLength(v: Vector2D) {
    return Math.sqrt(v.x * v.x + v.y * v.y);
}

interface NamedVector { // extends Vector2D로도 가능
    name: string;
    x: number;
    y: number;
}

const v: NamedVector = { x: 3, y: 4, name: 'Zee' };
calculateLength(v); // 정상, 결과는 5

 

아이템 5: any 타입 지양하기

any 타입 사용시 타입 체커와 타입스크립트 언어 서비스를 무력화시킨다.

따라서 최대한 사용을 피할 것

  1. any 타입에는 타입 안정성이 없다.
    age는 number 타입으로 선언되었지만, as any를 사용하면 string 타입을 할당할 수 있게 된다.
    따라서 아래처럼 선언될 가능성이 있다.
    ex. age += 1 // 런타임에 정상, age는 “121”
  2. any 타입에는 언어 서비스가 적용되지 않는다.
    => ts 모토:
    확장 가능한 자바스크립트

타입이 있다면 타입스크립트 언어 서비스는 자동완성 기능과 적절한 도움말을 제공한다.

그러나 any 타입은 제공하지 않는다.

이름 변경 서비스도 제공하지 않는다.

interface Person {
    first: string;
    last: string;
}

const formatName = (p: Person) => `${p.first} ${p.last}`
const formatNameAny = (p: any) => `${p.first} ${p.last}`

--- 여기서 Rename Symbol을 클릭 해 이름 변경 ---
프로젝트 내의 심벌 이름이 모두 변경됨

3. any 타입은 코드 리팩터링할 때 버그를 감춘다.

4. any는 타입 설계를 감춰버린다.

5. any는 타입시스템의 신뢰도를 떨어뜨린다.

 

Android 앱 개발을 하다보면 보안을 위해서 출시 전에는 꼭 난독화를 해야한다.

 

 

Android 소스 난독화란?

 

앱의 소스 코드를 난독화하는 기술로,

안드로이드 앱의 소스 코드를 분석하기 어렵게 만들기 위한 기술입니다.

이를 통해 소스 코드를 더욱 안전하게 보호할 수 있습니다.

 

해커가 앱의 코드를 이해하고 악성 기능을 추가하는 것을 어렵게 만들 수 있습니다.

그렇기 때문에 앱을 배포하기 전 앱 소스 난독화는 꼭 필요합니다.

 

 

 

Proguard 적용하기

 

 

Proguard는 소스코드를 난독화 및 최적화 해주는 무료 오픈소스 툴입니다.

 

minifyEnabled

축소, 난독화 및 코드 최적화를 활성화 / 비활성화 한다.

 

getDefaultProguardFile("proguard-android.txt")

Android Gradle 플러그인과 함께 패키지된 기본 Proguard 규칙 파일이 포함되어 있다. 

 

proguard-rules.pro 

프로젝트 레벨에 proguard-rules.pro 파일을 만들고 축소, 난독화 제외 클래서, shrink 여부 등에 관한 룰을 작성한다.

 

 

dex2jar 라이브러리를 사용해서 디컴파일하기

 

1. dependency 설치

brew install apktool dex2jar

 

2. apk 빌드

난독화한 앱 apk 빌드 파일을 가져옵니다.

 

3. 디컴파일

cd /path/to/apk/file
apktool d -s -o decompile app-release.apk

 

4. dex 파일을 jar로 변경

d2j-dex2jar classes.dex
d2j-dex2jar classes2.dex
d2j-dex2jar classes3.dex

 

5. jar파일을 zip파일로 변경

mv classes-dex2jar.jar classes-dex2jar.zip
mv classes2-dex2jar.jar classes2-dex2jar.zip
mv classes3-dex2jar.jar classes3-dex2jar.zip

 

6. zip 파일 해제

finder에서 zip 파일을 더블클릭하여 압축을 해제합니다.

 

7. 코드 확인

압축 해제된 디렉터리에서 파일 내용을 확인해봅니다.

아래처럼 사람이 읽을 수 없다면 성공한 것이다 !!!!

 

 

앱 출시를 위해서 난독화해본 건 처음이라, 디컴파일하면서 많이 애먹었다.

누군가 이 글을 보고 많은 도움이 되었으면 좋겠다 ㅎㅎ

+ Recent posts