Skip to content
Open
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,6 @@ wheels/
*.mmd
.idea
.omc

# assignments
assignments/latteeea/week3/cache/
142 changes: 142 additions & 0 deletions assignments/latteeea/week3/data/X-Username.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
## **왜 고치니까 잘 됐는지 (상세 정리)**

### **1. 클라이언트가 하고 있던 일**

이전 코드는 대략 이렇게 동작했습니다.

- **`GET /api/users/profile`**
- `Authorization: Bearer <액세스토큰>`
- 여기에 더해 **`X-Username: <AsyncStorage에 저장된 아이디>`** 를 붙였습니다.
- **`GET /api/users/me/agreements`**
- URL 안에 이미 **`/me/`** 가 있어서 “**지금 로그인한 나**”를 가리키는 전형적인 REST 패턴인데,
- 마찬가지로 **`X-Username`** 을 넘기는 경우가 있었습니다 (`getUserAgreements({ username })`).

즉, **“나 자신”을 조회하는 API**인데도, **헤더로 또 한 번 “이 사용자 이름이다”라고 보내는 이중 정보**가 있었습니다.

---

### **2. 서버 입장에서 어떻게 해석될 수 있는지**

백엔드 구현에 따라 다르지만, 흔한 패턴은 다음과 같습니다.

1. **JWT만 본다**
- 토큰 안의 `sub` / `user_id` 등으로 “누구인지” 결정하고, 그 사용자의 프로필·약관 동의를 돌려준다.
2. **`X-Username`이 있으면 그걸 우선한다 / 검증한다**
- “헤더에 적힌 username으로 프로필을 찾는다”
- 또는 “JWT의 사용자와 **헤더의 username이 같은지** 검사한다”
3. **불일치·잘못된 값이면 404**
- “해당 사용자를 찾을 수 없음”으로 처리하는 API도 있습니다 (401이 아니라 404인 경우).

그래서 **토큰은 유효한데** (`/api/auth/me` 200), **프로필/약관만 404**가 나는 현상이 나올 수 있습니다.

---

### **3. 왜 iOS는 되고 안드로이드만 안 됐을 가능성이 큰지**

**같은 JS 코드**인데 한쪽만 깨지는 이유는, 보통 **“보내는 문자열이 미묘하게 다르다”** 쪽입니다.

- **`X-Username`에 실려 가는 값**은 `AsyncStorage`에서 꺼낸 `username`입니다.
- 저장 시점은 로그인 직후 `setItem('@username', trimmedUsername)` 등인데,
- 예전에 다른 키(`username`, `@insole_app:username`)에 **다른 형태**로 저장된 값이 남았거나,
- **앞뒤 공백**, **보이지 않는 문자**, **대소문자** 등이 OS/스토리지 구현 차이로 달라질 수 있습니다.
- `getUsername()`은 **여러 키를 순서대로** 보는데, **첫 번째로 읽힌 값**이 “로그인한 사람과 같은 사람”이 아닐 수도 있습니다.

이때:

- JWT 안의 사용자 = **방금 로그인한 계정** (서버는 이걸로 `/auth/me`는 성공)
- `X-Username` = **다른 문자열** 또는 **서버 DB에 없는 조합**

이면 서버가 “이 username으로는 프로필을 못 찾겠다” → **404**를 줄 수 있습니다.

iOS에서는 우연히 **같은 키·같은 문자열**이 잘 맞았고, 안드로이드에서는 **스토리지/키 우선순위/문자열**이 달라져서 **불일치**가 났을 가능성이 큽니다.

(재설치 후에도, 앱이 쓰는 키가 여러 개라면 **다른 경로로 예전 값이 들어가는** 경우도 생깁니다.)

---

### **4. 이번 수정이 맞는 이유 (본질적인 정리)**

| **API** | **의미** |
| --- | --- |
| `/api/users/profile` (본인 조회) | 보통 **JWT만**으로 “현재 사용자”를 특정합니다. |
| `/api/users/me/agreements` | URL의 **`me`** 가 이미 “토큰의 나”를 의미합니다. |

여기에 **`X-Username`을 추가로 보내는 것**은:

- 서버가 이를 **무시**하면 다행이지만,
- **검증/우선 적용**에 쓰이면, JWT와 **한 글자라도 어긋날 때** 이상 동작(404 등)을 유발할 수 있습니다.

그래서 이번에 한 것은:

1. **`getProfile()`**
- **`Authorization`만** 사용 → “토큰이 가리키는 사용자”의 프로필만 조회.
- AsyncStorage username에 의존하지 않음 → **OS별 저장값 차이**에 덜 흔들림.
2. **`getUserAgreements()`**
- **`/users/me/...`** 는 **JWT 기준 ‘나’**만 보면 되므로 `username` 옵션(→ `X-Username`) 제거.
3. **`getUsername()`의 `trim()`**
- 나중에 다른 API에서 `X-Username`이 필요할 때, **공백만 다른 값**으로 잘못 나가는 것을 줄이기 위한 방어.

정리하면, **문제의 핵심은 “네트워크가 안드로이드만 막혔다”가 아니라, “내 정보 조회인데 `X-Username`으로 인한 사용자 식별 불일치 가능성”**이었고, **`/me`·본인 프로필은 JWT만 쓰도록 맞춘 것**이 해결책이 된 것입니다.

- 기존 → 변경 코드
- utils/api/profile.ts

```tsx
export async function getProfile(): Promise<ApiResponse<GetProfileResponse>> {
const username = await getUsername();
if (!username) {
throw new Error('사용자 이름을 찾을 수 없습니다.');
}

return apiGet<GetProfileResponse>(ENDPOINTS.USERS.PROFILE, {
username,
});
}

// 바꾼 버전
export async function getProfile(): Promise<ApiResponse<GetProfileResponse>> {
return apiGet<GetProfileResponse>(ENDPOINTS.USERS.PROFILE);
```

- app/(tabs)/profile/index.tsx

```tsx
if (foundUsername) {
try {
const agreementsResponse = await getUserAgreements({ username: foundUsername });
if (agreementsResponse.success && agreementsResponse.data) {
const agreedTerms = agreementsResponse.data.agreements.filter(
(agreement) => agreement.agreed
);
setUserAgreements(agreedTerms);
}
} catch (error) {
const err = error as ApiError;
if (err?.requiresAdditionalInfo === true && Array.isArray(err.missingFields) && err.missingFields.length > 0) {
setAdditionalInfoResume(err.missingFields);
router.replace(getFirstAdditionalInfoRoute(err.missingFields) as any);
return;
}
console.error('약관 동의 상태 조회 실패:', error);
}


// 바꾼 버전

try {
const agreementsResponse = await getUserAgreements();
if (agreementsResponse.success && agreementsResponse.data) {
const agreedTerms = agreementsResponse.data.agreements.filter(
(agreement) => agreement.agreed
);
setUserAgreements(agreedTerms);
}
} catch (error) {
const err = error as ApiError;
if (err?.requiresAdditionalInfo === true && Array.isArray(err.missingFields) && err.missingFields.length > 0) {
setAdditionalInfoResume(err.missingFields);
router.replace(getFirstAdditionalInfoRoute(err.missingFields) as any);
return;
```

- X-Username 검증 부분 제거
97 changes: 97 additions & 0 deletions assignments/latteeea/week3/data/android_build_error.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
>
>
> - 원인: 일부 파일/폴더가 다른 소유자/권한 상태로 남아있어서(복사, 압축 해제, 깃/툴, 권한 상속 끊김 등) EAS가 접근 실패
> - 해결: 프로젝트 루트에 대해 **권한 상속 활성화 + 내 계정 Full Control 재부여**

ondrive/desktop 에 프로젝트 폴더 사용하다가

onedrive 동기화 되는게 귀찮고 폴더 경로에 따른 권한 에러가 많이 나서 ondrive 동기화를 중지 시킴

하고 ondrive/desktop 에 있던 프로젝트를 c:/users/kimta/desktop 에 옮김

ios 빌드와 auto submit 까지는 잘 됐는데 안드로이드 빌드할 때 prepare project 단계에서 경로를 못 찾는 에러 발생 (권한)

```jsx
tar: .idea/.gitignore: Cannot open: Permission denied
tar: .idea/insole-app.iml: Cannot open: Permission denied
tar: .idea/inspectionProfiles: Cannot mkdir: Permission denied
tar: .idea/misc.xml: Cannot open: Permission denied
tar: .idea/modules.xml: Cannot open: Permission denied
tar: .idea/vcs.xml: Cannot open: Permission denied
tar: .idea/workspace.xml: Cannot open: Permission denied
tar: .vscode/extensions.json: Cannot open: Permission denied
tar: .vscode/settings.json: Cannot open: Permission denied
tar: app/(tabs): Cannot mkdir: Permission denied
tar: app/account-exists.tsx: Cannot open: Permission denied
tar: app/find-account.tsx: Cannot open: Permission denied
tar: app/index.tsx: Cannot open: Permission denied
tar: app/login.tsx: Cannot open: Permission denied
tar: app/modal.tsx: Cannot open: Permission denied
tar: app/register: Cannot mkdir: Permission denied
tar: app/social: Cannot mkdir: Permission denied
```

tar 압축을 푸는 것에 대한 에러였는데 일단 권한 에러라서 desktop이 아닌 C:/dev/{프로젝트} 로 아예 C 하위로 옮겨봤다 (desktop이 보호 폴더인 경우가 많다고 지피티가 그럼)

powershell 관리자 권한으로 실행해서 다시 해봤는데 또 같은 에러로 실패 → desktop 문제 아님

`tar -cf test.tar .` ← 이 명령어로 tar 이 되는지 확인해봤는데 잘되는 것 확인

로컬에서 permission 에러 안나니까 이제 되겠지 → eas build 시도 → but 실패

⇒ EAS CLI의 문제다! EAS가 내부적으로 다른 tar 를 쓰거나 EAS가 만드는 임시 폴더/아카이브 과정에서 권한이 막히는 것

→ EAS CLI가 프로젝트를 임시 작업 폴더에 풀어놓고(스테이징) tar 작업을 하는데 그 임시 폴더 경로가 쓰기 권한이 없는(구: onedrive 경로)곳으로 잡혀있어서 tar가 디렉토리 생성/파일 쓰기를 못 하는 상황

TEMP/TMP 경로 확인해봤더니 onedrive에 있는게 아니라 C 하위의 appdata/local에 정상으로 위치해 있음. 이게 문제가 아니고

⇒ tar 에서 permission denied가 되는게 아니라, 파일/폴더의 NTFS 권한/속성(ACL/읽기전용/소유자) 이 꼬여있는 상태

→ powershell 관리자 권한으로

1. 읽기 전용 속성 제거 `attrib -R * /S /D`
2. 소유권을 현재 사용자로 강제 지정 `takeown /F . /R /D Y`
3. 권한을 현재 사용자 full control로 재부여 + 상속 켜기 `icacls . /inheritance:e /grant:r "$env:USERNAME:(OI)(CI)F" /T`

하고 eas build했더니 성공

### 정리하자면

ondrive/desktop이 바탕화면에 보이게 해둔 상태에서 동기화를 끊으니까 바탕화면에 바로가기로 되어 있던 폴더나 프로그램들이 실행할 수 없다고 뜬 적이 있음 → 여기서 계정에 대한 상속 체인이 변경되었을 것으로 예측

동기화를 끊을 시 윈도우는

- 경로 재지정을 원래 위치로 되돌리거나
- ondrive 폴더를 분리하거나
- 로컬 복사하거나 하는데

이 과정에서 내가 겪은 증상을 보자면 두가지 가능성이 있음

- 대상 경로가 바뀌어서 링크가 깨짐 (onedrive 경로 기반으로 바로가기면, 동기화 해제 시 대상이 사라져서 실행 불가)
- 파일은 있는데 접근 권한이 달라짐 (SID/ACL 문제로 엑세스 거부된 상태)

이게 eas 문제로 이어진 이유

- onedrive desktop 하위에서 생성된 파일들을
- OneDrive 리디렉션 해제 과정에서 파일/폴더의 소유자 SID 또는 ACL 상속 구조가 변경되었고,
- 이후 동일 볼륨 내 이동(C:/dev 하위)은 그 상태를 그대로 유지했을 가능성이 높다.

## 📌 문제 발생 흐름 정리

1. OneDrive가 Desktop을 리디렉션한 상태에서 프로젝트를 생성
2. OneDrive 동기화를 해제하면서:
- Desktop 경로 재지정 해제
- 파일 재배치/복사/분리 과정 발생
- 이 과정에서 일부 파일의 **소유자 SID 또는 ACL 상속 체인이 변경되었을 가능성**
3. 이후 동일 C: 드라이브 내에서 `C:\dev`로 이동
- NTFS는 같은 볼륨 이동 시 ACL을 그대로 유지
- 즉, 이미 비정상 상태였던 권한 구조가 그대로 유지됨
4. EAS Build가 프로젝트를 스테이징하면서 디렉토리 생성/복사 단계에서 `Permission denied` 발생

---

## 📌 왜 일반 tar는 되고 EAS만 실패했을까?

- 일반 `tar`는 “읽기 중심”
- EAS는 스테이징 디렉토리 생성 + 복사 + 권한 처리 수행
- 소유자/ACL 상속 구조가 비정상이면 mkdir 단계에서 실패
44 changes: 44 additions & 0 deletions assignments/latteeea/week3/data/android_network_security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
개발 환경: react native로 크로스 플랫폼 개발 중, 하지만 네이티브 모듈(블루투스 통신)을 사용해서 expo go 앱을 통해서는 빌드하지 못하고 android/ios 각각 빌드를 진행하고 있었다. profile development로 해서 기기 시뮬을 돌릴 때는 개발 기기의 네트워크와 시뮬 기기의 네트워크가 같은 것에 연결되어 있어야 되는 것은 알고 있었음 (다만 이유는 뭔지 모르겠다)

문제 상황: 클라이언트측과 QA를 위해 profile preview로 apk 파일을 뽑았음. 로컬 프로세스 돌려가지고 같은 네트워크 연결할 때는 몰랐는데 apk 파일 뽑고 같은 네트워크 유지하면서 버튼 누르는데 ‘서버에 연결할 수 없습니다. 실제 기기를 이용하는 경우 로컬 IP를 사용해야 합니다) 라고 뜸 ????? 상황을 정리하자면

- ios ipa 파일을 testflight에 임시 배포했을때 다른 사람 폰에서 시뮬 돌린건 잘 됐음 (코드 로직의 문제는 아님)
- 다만 도메인 주소를 연결할 수 없는 상황이고 서버를 이관 혹은 폐기하고 새롭게 만들 예정이라 임시 IP 주소를 https 로 전환하지 않은 상태임 → 이게 개크게 의심됨 (왜냐면 여태 프젝할때는 항상 https 로 배포된 서버에 연결해서 실제 기기 시뮬 돌렸기 때문)

해결을 시도해보자

- 안드로이드 9부터 HTTP 요청 차단이 기본 정책 → usesCleartextTraffic : True 인지 확인하기 (응 true 야~)
- http 배포된 서버는 80 포트 디폴트로 사용하는데 앱이 지 혼자서 8000포트로 가고 있는거 아닌가? (포트를 명시적으로 고정해볼까? 생각함) 8000으로 고정해서 브라우저 열어보려고 하면 안되는거 확인 (ㄹㅇ 포트를 8000으로 쓰나?)
- 안드로이드 브라우저에서 http://{IP주소} 로 들어가는건 들어가짐! 이건 서버의 방화벽이나 보안그룹 문제는 아니고 앱 문제가 맞다! 판단
- 근데 usesClearTextTraffic 적용되어 있는데 왜 안되지? androidmanifest에 안들어갔나? (빌드할때 설정을 건너뛴다던가 다른 설정이 얘를 막는다던가)
1. 포트 80으로 고정하기 (nginx 설정)
1. 명령어를 계속 쳐보려고 했지만 ios 시뮬 돌리는 과정에서 api 요청이 8000으로 가는건 아니고 포트를 지정안하고 api 요청을 날리는 것을 확인함 (포트 문제가 아닐 수 있겠다) 앱이 8000을 찍는 건 아님 판단
2. **cleartext 허용이 빌드에 반영이 안된 것이 궁극적인 문제임!!**
1. **network security config 강제 넣기 + manifest도 수동으로 넣어주기 (app.json 이 아니라 android 폴더 내의 파일에 설정하는 거기 때문에)**

해결책 정리

- 일단 이게 ios가 아닌 안드로이드에서만 터진 이유 : Cleartext (HTTP) 차단 정책
- Network Security Policy : 앱이 HTTP로 통신하는 걸 기본적으로 금지하는 방향
- 브라우저로 열리는데 앱이 통신 못하는 이유는?
- 브라우저는 : 사용자 앱이라 HTTP 허용 정책이 다름
- 우리 앱 : OS가 cleartext 허용했나? 물어보고 막아버림 → 연결 실패
- usesCleartextTraffic 이 적혀있는데도 안된 이유?
- android 에서 cleartext 허용을 결정하는 방식은 ‘레이어’가 있다
- manifest의 usesCleartextTraffic = “true”
- Network Security Config
- 기기/OS 정책, 라이브러리 동작 등

⇒ usesCleartextTraffic=true면 풀릴때가 많고 나의 경우에는 그게 최종 apk 파일에 제대로 반영이 안된 경우임.

- 특히 expo/eas 환경에서는
- app.json 설정이 빌드 과정에서 네이티브 파일을 생성/수정하면서 반영되는데,
- 프로젝트 상태(프리빌드, 플러그인, 빌드 캐시, 다른 매니패스트 병합 결과)에 따라 내가 켰다고 생각한 값이 최종 androidManifest 에 안들어가는 일이 생긴 것
- 결국에는 HTTPS로 가는 게 정석이다!! (android/ios 둘다 정책 신경 쓸 일이 줄어들긔)
- 이 방식은 QA 같은 곳에서 임시로 사용하기
- expo prebuild 를 돌리면 파일이 다시 날아갈 수 있으니 이건 유의하기!

결론

- app.json 에 usesclear 옵션이 적용되어 있는데 왜 빌드할때 적용이 안됐냐? → 빌드 시점에 androidManifest를 생성/패치하는데 prebuild를 이미 해둔 상태거나 다른 플러그인이 manifest를 덮어쓰는 경우 안들어갈 수 있음
- https 로 바꾸면 거의 백프로 해결되는 문제긴 해~ 결국엔 해결될 수 있는 문제였지만 네트워크에 대해 좀 알게된 경험이었다!
Loading