[react-query] 리렌더링 시 useMutation 의 동작과 주의사항

useMutation 은 useEffect, useCallback 같은 hook 과는 다르게 의존성을 설정할 수 없다. 그럼, 리렌더링 시에 useMutation에서 반환되는 mutate() 는 항상 새로 만들어 지는 걸까?

useMutation 에 파라메터로 넘기는 mutaionFn, onMutate, onSettled, onSuccess 등이 리렌더링 시 변경될 경우, mutate() 를 호출 하면 각 상황에서 변경된 함수가 호출되는 것으로 봐서 그럴 수도 있어 보인다.

그렇다면 mutate() 를 호출 해서 mutationFn 이 이미 실행 중인 상황에서 리렌더링이 일어나 onSuccess 등이 변경 되고, 그 후 mutationFn 의 실행이 완료된다면 어떻게 될까? mutationFn 이 성공적으로 수행된 후 호출 되는 onSuccess 는 앞서 mutate() 가 호출 되었을 당시의 onSuccess 함수가 아니라 리렌더링 중 변경된 onSuccess 함수가 호출된다.

실제 react-query 가 어떻게 구현되어 있는지 살펴보자.

  const [observer] = React.useState(
    () =>
      new MutationObserver<TData, TError, TVariables, TContext>(
        queryClient,
        options,
      ),
  )

useMutation 의 구현을 보면 observer 라는 state 를 만들어 쓰는 것을 볼 수 있다.

  React.useEffect(() => {
    observer.setOptions(options)
  }, [observer, options])

useMutation 에 파라메터로 넘어가는 옵션이 변경될 경우, 이 observer state 의 setOptions() 를 호출 하여 변경해 주게되며,

  const mutate = React.useCallback<
    UseMutateFunction<TData, TError, TVariables, TContext>
  >(
    (variables, mutateOptions) => {
      observer.mutate(variables, mutateOptions).catch(noop)
    },
    [observer],
  )

우리가 useMutation() 의 결과로 반환받는 mutate 는 useCallback() 을 사용하여 만드는데, 의존성 목록에는 observer 만 들어가 있어 mutate 는 새로 만들어지지 않는다.

좀 더 안 쪽을 살펴 보자. observer 의 mutate() 를 호출 했을때, 즉, 우리가 useMutation()에서 반환 받은 mutate() 함수를 호출 했을 때 무슨일이 일어나는걸까?

  mutate(
    variables?: TVariables,
    options?: MutateOptions<TData, TError, TVariables, TContext>,
  ): Promise<TData> {
    this.mutateOptions = options

    if (this.currentMutation) {
      this.currentMutation.removeObserver(this)
    }

    this.currentMutation = this.client.getMutationCache().build(this.client, {
      ...this.options,
      variables:
        typeof variables !== 'undefined' ? variables : this.options.variables,
    })

    this.currentMutation.addObserver(this)

    return this.currentMutation.execute()
  }

mutate() 가 호출 되는 시점에 현재의 옵션을 넘겨서 Mutation 객체를 새로 만들고, 마지막에 Mutation.execute() 함수를 호출해 준다.

async execute(): Promise<TData> {
    const executeMutation = () => {
      this.retryer = createRetryer({
        fn: () => {
          if (!this.options.mutationFn) {
            return Promise.reject('No mutationFn found')
          }
          return this.options.mutationFn(this.state.variables!)
        },
...
      })

      return this.retryer.promise
    }

...
      const data = await executeMutation()

      // Notify cache callback
      await this.mutationCache.config.onSuccess?.(
        data,
        this.state.variables,
        this.state.context,
        this as Mutation<unknown, unknown, unknown, unknown>,
      )
...
  }

Mutation.execute() 는 생성 시 넘겨 받았던 options의 mutationFn 을 호출 하고, 그 결과에 따라 onSuccess 등을 호출 해 준다.

여기까지 봤을때는, mutate() 호출 시 객체를 생성하고, 필요한 정보는 해당 객체가 다 가지고 있는 형태이므로 이후, useMutation() 이 새로 호출되고 옵션이 바뀌더라도, 현재 실행중인 mutate 에는 영향을 미치지 않을 것으로 보인다.

그런데 아직 보지 못한 곳이 있다. observer의 setOptons().

  setOptions(
    options?: MutationObserverOptions<TData, TError, TVariables, TContext>,
  ) {
    const prevOptions = this.options
    this.options = this.client.defaultMutationOptions(options)
    if (!shallowEqualObjects(prevOptions, this.options)) {
      this.client.getMutationCache().notify({
        type: 'observerOptionsUpdated',
        mutation: this.currentMutation,
        observer: this,
      })
    }
    this.currentMutation?.setOptions(this.options)
  }

뭔가 이유가 있겠지만, 개인적으로는 좀 의아한데… 마지막에 현재 mutation, 즉, 마지막으로 mutate() 호출될 당시의 mutation 객체의 옵션을 같이 바꿔준다.

이로인해, mutaionFn 이 실행 중 useMutation의 옵션이 바뀌게 되면, mutationFn 이 끝난 후 처리는 바뀐 옵션대로 하게되는 것이다.

여기서 useMutation() 사용할때 한 가지 주의 사항이 생긴다. mutate()를 호출한 후 useMutation()의 옵션이 변하는 경우에 대해 고려하라는 것.

의도한게 아니라면, mutate() 호출이 완료되기 전에 useMutation() 의 옵션이 변경된는 상황은 막아야 한다. 보통 react-query 를 사용하는 목적을 생각해보면, mutationFn 을 API 호출 작업일 것이고, 해당 작업은 서버나 네트워크 상황 등에 따라 매우 느리게 끝날 가능성이 있다. 그리고, 그 사이 useMutation()의 옵션에 영향을 주는 state 가 변경되어 리렌더링이 일어나면, mutationFn 이 완료되었을 때는 의도치 않은 작업이 수행될 수 있다.

React native + axios 로 json + image multipart 데이터 전송하기

React native 에서 사용되는 FormData 는 form-data 모듈과는 뭔가 다른지, multipart로 전송할 경우에 각 파트의 content-type 을 지정할 수 없어서 애를 먹었다. (디폴트 application/octect-stream 으로 설정됨)

이미지는 uri 프로퍼티에 파일경로를 type 프로퍼티에 타입을 지정해서 객체로 넘기면 된다는 거까지는 쉽게 알아냈는데, JSON은 도통 방법을 찾을 수가 없다.

거의 포기할려던 찰나… string 프로퍼티에 JSON을 문자열로 변환하여 넘기면 된다는 내용을 발견! 해결되었다.

import axios from "axios";

exprot const submitMultipart = (body: any) => {
  const formData = new FormData();

  formData.append("jsonData", {"string": JSON.stringfy(body.jsonData, type: "application/json"});
  formData.appand("image", {uri: body.imageUri, type: "image/jpeg"});

  const instance = axios.create();
  return await instance({
    url: "/post-data",
    method: "post",
    data: formData,
    headers: {
      "content-type: "multipart/form-data"
    }
  });
}

참고 : https://stackoverflow.com/questions/32441963/how-to-use-formdata-in-react-native

[GitHub Actions] 컨텐츠 NuGet 패키지 생성 및 배포하기

C# 프로젝트 진행 중 다른 웹프로젝트의 결과물을 포함해야 되는 상황에 직면했다. 여러가지 방향을 고민 했지만, 최종적으로 결정된건, 해당 웹프로젝트의 결과물을 NuGet 패키지로 만들어서 GitHub의 NuGet Registry 로 배포하고, 이 패키지를 C# 프로젝트에 포함하는 방식.

우선 웹프로젝트의 GitHub 레파지토리에 .github/workflows 폴더에 actions 스크립트를 추가한다.

앞부분에는 프로젝트에 따라 결과물을 생성할 수 있는 빌드 스크립트를 적당히 추가해주고, 이어서 다음과 같이 NuGet.exe 를 사용한 패키지 생성 및 배포 스크립트를 추가한다.

GitHub 에서 해당 레파지토리의 NuGet Registry에 배포를 할 경우, ApiKey 로 secrets.GITHUB_TOKEN을 사용하면 된다.

...

jobs:
  build:
    steps:

      ...

      # NuGet.exe 설치
      - name : Setup NuGet
        uses: NuGet/setup-nuget@1.0.7
        with:
          nuget-version: latest
 
      # NuGet 패키지 생성
      - name: Create NuGet package
        run: nuget pack

      - name: Upload NuGet package
        run: nuget push *.nupkg -configfile NuGet.config -ApiKey ${{ secrets.GITHUB_TOKEN }} -Source "github" -SkipDuplicate

-SkipDuplicate 옵션을 사용하면, 해당 패키지의 버전이 이미 Registry에 존재하더라도 Actions 스크립트의 실행이 실패하지 않도록 하여 주므로, 평소에는 패키지 배포 때문에 Actions 실행이 실패하지 않으면서도 실제 배포가 필요한 경우에만 패키지 버전을 변경하는 방식으로 배포여부를 제어할 수 있다.

NuGet 패키지를 생성하기 위해서는 루트 폴더에 다음과 같이 .nuspec 파일을 만들어 주어야 한다.

<?xml version="1.0"?>
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
	<metadata>
		<id>[Company].[Project].[PackageName]</id>
		<version>1.0.0</version>
		<authors>[Company]</authors>
		<owners>[Company Name]</owners>
		<projectUrl>https://www.company.com</projectUrl>
		<repository type="git" url="https://github.com/[owner]/[repository].git" />
		<description>My Company NuGet Package</description>
		<requireLicenseAcceptance>false</requireLicenseAcceptance>
		<copyright>Copyright (c)2022 My Company corp.</copyright>
		<dependencies></dependencies>
		<contentFiles>
			<files include="any\any\**" buildAction="EmbeddedResource" copyToOutput="true" flatten="false"/>		
		</contentFiles>
	</metadata>

	<files>
		<file src="build\**" target="contentFiles\any\any\WebApplication"/>
	</files>
</package>

<id> 는 다른 패키지와 겹치지 않도록 적당한 규칙으로 부여해주면 되고, <repository> 에서 [owner] 에는 레파지토리 소유자 이름, [Repository]에는 해당 레파지토리 이름을 적어 줘야한다. 즉, 여기서는 Package를 업로드할 github 레파지토리의 URL을 적어주면된다. 나머지 항목들은 상황에 맞처 적당히 적어주면 된다.

<files>에서 패키지에 포함할 파일들을 지정한다. 여기서는 build 폴더 하위의 모든 항목을 패키지 내의 contentFiles\any\any\WebApplication 폴더 하위에 넣도록 지정했다. contentFiles 폴더는 패키지내에 컨텐츠 파일들을 저장하는 경로이고, any/any 는 모든 플랫폼과 버전에 해당하는 파일의 경로다.

여기서 또 중요한 건 <contentFiles> 항목이다. 여기서는 패키지 내의 컨텐츠 파일에 대한 설정을 하는데, 기본적으로 컨텐츠 파일의 경로는 패키지 내의 contentsFiles 폴더 하위다. 즉, 위의 <files> 에서 지정한 파일들이 대상이 되는 것이다.

여기서는 any\any 폴더 하위의 모든 파일과 폴더에 대해 빌드 결과물 폴더로 복사하고(copyToOutput=”true”), 복사할때 폴더 구조를 그대로 유지하도록 (flatten=”false”) 설정했다.

이제 패키지를 업로드할 NuGet Registry 설정을 추가하기 위해 프로젝트 폴더 루트에 NuGet.config 파일을 생성하자.

<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <packageSources>
        <clear />
        <add key="github" value="https://nuget.pkg.github.com/[owner]/index.json" />
    </packageSources>
</configuration>

여기서 <packageSources> 의 <add> 에서 정의한 key의 값이 앞서 Actions 스크립트에서 nuget push 시 지정한 -Source 옵션의 값이다. .nuspec 파일과는 다르게 여기서는 URL은 nuget.pkg.github.com 으로 하여야 하며, 패키지를 배포하는 registry 의 owner 를 [owner] 에 적어주면되다.

여기서 주의 할 것은, 패키지를 생성하는 레파지토리와 NuGet Registry의 owner 가 일치할 경우에만, nuget push 시에 secrets.GITHUB_TOKEN 을 ApiKey 로 사용할 수 있다는 것이다. 각각의 owner 가 다를 경우에는 권한이 있는 계정의 Peronal Access Token 을 생성하고, 프로젝트의 레파지토리 설정에서 secret으로 추가하는 작업을 해주어야 한다.(이경우는 -ApiKey 옵션이 아니라 -username 과 -password 옵션을 사용하여야 한다)

세 파일을 레파지토리에 추가하고 GitHub 레파지토리로 push 해 주면 해당 프로젝트의 결과물을 NuGet 패키지로 배포할 수 있는 준비가 끝난다.

언리얼 엔진 5 – 프로젝트 패키징 안되는 문제 (SDK 설치하기)

언리얼로 만든 결과물을 최종적으로 윈도우용으로 배포할 수 있게 패키징을 하려면, 상단 툴바에서 [플랫폼] -> [Windows] -> [프로젝트 패키지] 를 클릭하면 된다.

으잉? 근데 안된다….

아까 프로젝트 패키지 를 실행할때도 하단에 SDK 관련해서 에러 메시지 같은게 있더라니…

그래서 [계속] 을 클릭해줘면… 역시나 에러 메시지만 뜰 뿐….

그래서 계속 시도해 봤지만 여전히 에러메시지만 볼 수 있을 뿐이었다. 그래서, 이 놈의 SDK는 도대체 왜 설치가 되지 않았는가.. 하며 이거저거 뒤지다 보니…

이런… 필요한건 언리얼 SDK가 Windows SDK 였다. 으… 이런건 설치할 때 알아서 깔아 주거나 안내를 해줘야지…

… 내가 메시지를 제대로 안 봤나;;;;;

아무튼… Windows SDK가 필요하다니 마이크로소프트 홈페이지로 가서 설치파일을 구하자

  • Windows SDK :

https://developer.microsoft.com/en-us/windows/downloads/windows-sdk/

위 링크로 들어가서 스크롤을 좀 내리면 [Download the installer] 라는 버튼이 있다.

요걸 클릭해서 설치 파일을 다운 받아 실행 한 후, Next 와 Download 를 클릭해면 끝.

그리고 왜 때문인지 .NET 도 설치가 필요하다고 한다.

  • .NET Core 3.1 :

https://dotnet.microsoft.com/en-us/download/dotnet/3.1

음.. 이걸 설치 안 하고는 테스트를 안 해봤는데, 설치가 불필요할 수도 있으니, Windows SDK를 설치해도 안 되는 경우에 시도해 보도록 하자.

설치된 윈도우 버전에 따라 64 bit 버전은 [x64], 32 bit 버전은 [x86] 을 클릭하여 설치파일을 다운 받은 후 설치 해주면 된다.

그리고, 다시 언리얼 엔진 으로 돌아와 프로젝트 패키지가 있는 메뉴를 열어보면…

아래 쪽 SDK 관련된 부분이 바뀐걸 볼 수 있다. 이제 프로젝트 패키지를 실행해보자.

아래쪽 구석에 패키징이 진행 중이라는 메시지가 뜨고, 아~주 조금만 (물론 패키징할 크기와 PC 성능에 따라 많이 일수도) 기다리면…

패키징이 완료 됐다는 반가운 메시지를 맞이 할 수 있다.

혹시, 패키징 진행 중에 아래와 같은 경고 창이 뜨면 [액세스 허용]을 클릭해 주면 된다.

언리얼 엔진 5 Quixel Bridge – 로그인실패 및 “Failed to restart background service” 에러 수정하기

요즘 언리얼 엔진을 배운답시고 깔짝대고 있는데…

언리얼 엔진을 사용하는데 필수라는 Quixel Bridge가 제대로 작동이 안된다.

일단 로그인을 시도하면 자꾸 먹통이 되어버리고, 또, 왼쪽 구석에 “Failed to restart background service” 메시지가 자꾸 뜨는데, 요건 Restart now를 몇 번 누르다 보면 괜찮은거 같기도 하고…

백그라운드 서비스 머시기는 뭔지 모르겠고, 아무튼 일단 로그인이 되어야 뭐라도 될텐데 싶어서 구글의 힘을 좀 빌렸 봤으나 뾰족한 해결잭은 보이지 않았다.

그럼 백그라운드 머시기 에러라도 수정하려고 뒤지다보니

https://help.quixel.com/hc/en-us/community/posts/4409168256401–Failed-to-restart-background-service-after-UE5-Quixel-Bridge-update

개발자가 그 문제는 수정을 했으니 플러그인을 업데이트 하란다. 이미 난 최신버전인데…. 음.. 이것도 아닌가… 하면서 아래 댓글들을 읽다보니, 하라는대로 해서 해결이 됐데. 다시 개발자가 쓴 글을 보니 마지막 부분을 놓쳤더라고.

Quixel Bride를 실행하면 오른쪽 위에 사람 모양의 아이콘이 보이는데 요놈을 클릭해서 나오는 메뉴에서 Preferences를 클릭하면

설정화면 제일 위에 Library Path 가 보인다. 이게 설정이 잘못된게 원인인데… 이미 설정을 바꿔버려서 모르겠지만 아마 권한이 없는 경로가 설정되어 있었던듯.

적당한 경로에 폴더를 하나 만들어 주고,

설정을 해당 폴더로 바꿔주면..

짜잔~

더 이상 에러가 나지 않는다.

더군다나 로그인도 정상적으로 되네?

그냥 설치만하고 설정에 손도 안됐으니..

기본 설정이 잘못되어 있었던데다,

오류가 발생하는 원인도 제대로 알려주지 않고…. 뭔가 허접의 기운이 느껴지지만 …

아니, 뭐.. 어쨌든 문제 해결!

안드로이드앱이 항상 시작되도록 하기

근 1년 전에 안드로이드앱이 항상 실행되어 있도록 하기 위해서 썼던 방법.
(그 때 당시 자동 업데이트 기능으로 업데이트 후 다시 실행하는 것도 이 방법으로 해결했음)

우선 Activity 의 인스턴스가 1개만 존재하도록 해야 하므로 메인 activity의
lanuchMode를 singleTask로 설정해 준다.

<activity
            android:name=”.SampleActivity”
            android:label=”@string/app_name” android:theme=”@android:style/Theme.NoTitleBar.Fullscreen”
            android:launchMode=”singleTask”>

그리고, Service 를 하나 추가하고, Service에서 Alarm 으로  메인 Activity에 주기적으로 Intent를 날리도록 한다.

public class SampleService extends Service
{
    private static final int ALARM_INTERVAL = 30000;
    private boolean bRunning = false;

    public int onStartCommand(Intent intent, int flags, int startId) {
        if (!bRunning) {
            bRunning = true;                        
                       
            final Intent alarmIntent = new Intent(this, SampleActivity.class);
            final PendingIntent pender = PendingIntent.getActivity(this, 0, alarmIntent, 0);
                       
            long triggerTime = SystemClock.elapsedRealtime() + 1000*30;
                       
            final AlarmManager alarm = (AlarmManager) this
                                            .getSystemService(Context.ALARM_SERVICE);
            alarm.setRepeating(AlarmManager.ELAPSED_REALTIME, triggerTime,
                               ALARM_INTERVAL, pender);
        } else {
            Log.e(“hadpan”, “sample service thread is already running”);
        }

        return START_STICKY;
    }

}

해당 Activity가 singleTask 로 설정되었기 때문에 Activity가 존재 한다면, 화면이 활성화 되고,
아직 만들어지기 전이라면 새로 앱을 실행하게 된다.

alarm 주기는 필요에 따라 적당히 조절하면 되는데.. 사실 이런 alram을 쓰는거 자체가 별로 권장하는 방법은 아니다.
당시, 만들던 앱 자체가 단말기를 전용 단말기 처럼.. 그러니까 단말기에서 해당 앱만 동작하면 되는 형태여서..
(일종의 키오스크 형태)

이 방법을 사용했지만 그다지 좋은 방법이라고는 생각되지 않음…=_=

Windows에서 Eclipse CDT 로 EFL 개발 하기

필요한건 EFL 라이브러리, MinGW, Eclipse CDT 세 가지다.

MinGW, Eclipse CDT의 설치는 생략 하기로 하고….

우선, http://dev.enlightenment.fr/~doursse/NSIS/ 에서 윈도우용 설치파일을 다운 받는다.

현재 최신 버전은 1.7.4 이다.

설치파일을 실행한 후 대부분 [Next] 를 클릭해 주면 되는데.. 다음 단계에서는 주의!

사용자 삽입 이미지

처음에는 다 선택된 거 처럼 보이지만(필자는 그런줄 알았음=_=), 스크롤바를 내려보면 [EFL binaries] 만

선택이 되어 있다는걸 알수 있다. 왜 디폴트로 라이브러리들을 다 설치 안하는 걸로 해놨는지는 의문이다.

머가 먼지 잘 모르니까 그냥 처음부터 끝까지 다 클릭해서 체크를 한 다음 [Next] 를 클릭해 준다.

사용자 삽입 이미지

나머지는 역시 [Next]를 클릭해 주면 별 문제 없이 설치가 완료된다.

이제 Eclipse를 실행 하고 프로젝트를 만들어 보자. Eclipse에는 CDT가 설치 되어 있어야 한다.

프로젝트는 C Project 로 만들고, [Project Type]은 [Excutable] -> [Empty Project],

[Toolchains] 는 [MinGW GCC]를 선택해 주면 된다.

EFL 을 사용하기 위해서는 헤더파일과 라이브러리 설정을 추가해 주어야 한다.

[Project Explorer] 에서 방금 만든 프로젝트를 선택하고, 마우스 오른쪽 버튼를 누른 다음,

[Properties]를 클릭한다.

왼쪽 트리에서 [C/C++ General] -> [Paths and Symbols]를 선택한다.

[Includes] 탭을 선택하고 [Add…] 버튼을 클릭하여 다음 경로들을 추가 해준다.

여기서 C:\Efl 은 아까 EFL 라이브러리를 설치한 경로이니, 경로를 변경한 경우에는 바꿔서 적어주면 된다.

C:\Efl\include\elementary-1
C:\Efl\include\eina-1
C:\Efl\include\eet-1
C:\Efl\include\evas-1
C:\Efl\include\ecore-1
C:\Efl\include\edje-1
C:\Efl\include\evil-1
C:\Efl\include\e_dbus-1
C:\Efl\include\dbus-1.0
C:\Efl\lib\dbus-1.0\include
C:\Efl\include\efreet-1
C:\Efl\include\ethumb-1

이 때, 상단의 [Configuration]를 [ All Configurations ] 로 변경하고 추가 해주면,

Debug와 Release에 모두 추가된다.

일일이 추가 해야 되서 좀 귀찮으면 다이얼로그 하단에 보이는 [Import Settings…] 를 클릭해서

다음 파일을 import 해주면된다.

1280029520.xml

파일을 선택한 다음, 왼쪽에서는 설정을 추가할 프로젝트, 오른쪽에서는 설정을 추가할 configuration을

선택해 주면 된다. Debug와 Release를 동시에 할 수 없으니, 따로 두 번 해 주어야 한다.

사용자 삽입 이미지

다음은 [Library Paths]을 선택하고 [Add…] 버튼을 클릭해서 다음 경로를 추가 해준다.

C:\Efl\lib

마지막으로 [Libraries] 을 선택해서 [Add…] 버튼을 클릭하고, 사용할 라이브러리 를 추가 해준다.

앞에 까지는 항상 동일하지만, 이 탭은 프로젝트에서 실제 사용하는 라이브러리에 따라 달라질 수 있다.

머, 걍 다 써줘도 상관 없지만… 이 탭의 설정을 설정파일로 import도 안되기 때문에, 필요한 거만 써주는게 좋겠다.

EFL 중에서 사용하는 라이브러리 이름을 추가해 주면 되는데 예를 들면, ecore, evil, elementrary 같은 식이다.

사용자 삽입 이미지






elementary, evil, evas 등 은 거의 항상 들어 갈것이다.

이제 [OK] 버튼을 누르고, 코드를 작성한 후 빌드해주면 된다.

리눅스에서는 pkg-config 인가 하는 프르그램을 써서 일일이 헤더 경로나 라이브러리 지정을 안하고 쓰는거 같던데..

윈도에서도 그런 방법이 없는지는… 모르겠다=_=;

EFL Windows 용 설치 하기

이 글이 글을 참조하여 소스로 빌드해보려 하였으나…

이건 머… autogen 돌릴 때 에러나고, 글에서 말하는 디렉토리도 못 찾겠고…

결국 gg…

그러나 윈도용 설치 파일을 찾아서 결국 설치 성공했다ㅎㅎ

http://dev.enlightenment.fr/~doursse/NSIS/

설치할 때 디폴트로 라이브러리가 설치 안되도록 설정 되어 있다는 건 함정=_=

첨에 설치했는데 테스트고 머고 에러가 나서 헤맸는데,

뒤지다 보니 윈도에서 개발하는 동영상이 있어서 보다가 깨달았음;;;

어쨌든 테스트도 잘 도는 거 같고…

이제 EFL 함 해보까?

Tomcat 7 에서 jsessionId 가 URL에 붙지 않도록 하기

Tomcat 7 + Spring 3.0 사용 중인데,

언젠가 부터 URL 뒤에 ;jsessionid= 블라블라 가 붙어있다.

첨에는 별 신경을 안 썼는데, <c:url> 태그를 사용해서 만든 URL에 이게 붙는 바람에

조금 곤란하게 되는 일이 생겼다.

그래서 찾아보니, 서버에서 session 관리하는 방법 중에 URL에 session ID를 붙여서

하는 방법을 사용하기 때문이고, session 관련 기능을 쓰면 그렇게 된단다.

그래서 없애는 방법이 없나 좀 찾아 봤는데, Spring security 쪽에서 설정을 바꾸는 방법도 있다는데,

Spring security는 아직 쓰지도 않고 있으니 이건 아닌거 같고, Tomcat 7에서 web-app 설정을 추가해서

해결이 가능하다. 다음을 web.xml 에 추가해 주면 된다.

<session-config>
<tracking-mode>COOKIE</tracking-mode>
</session-config>

session 관리를 cookie를 통해서 하라고 설정하는 거 같다.

Servlet 3.0 이상에서 지원하는 거라서 Tomcat 7 이상이어야 되고, 첫 부분에 다음과 같이

버전을 3.0 으로 해줘야 한다.

<web-app version=”3.0″ xmlns=”http://java.sun.com/xml/ns/javaee”
xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance”
xsi:schemaLocation=”http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd”>

일단 해결은 됐는데, 뭔가 좀 찝찝하기도 함=_=

SeeAlso :
 * http://www.gotoquiz.com/web-coding/programming/java-programming/disable-url-session-ids-jsessionid-in-tomcat-7-glassfish-v3/

안드로이드 사용자 조작 없이 백그라운드에서 패키지 설치하기

개발 중인 앱에 자동업데이트 기능이 필요하다.
수십에서 수백대가 설치되는데다 사용자가 일일이 조작할 수는 없는 상황이기 때문에
프로그램이 알아서 파일을 다운로드 받고 패키지 설치까지 하는 기능이 필요한 상황.

하지만, 알아본 바로는 마켓의 자동 업데이트를 통하지 않는 다음에야 사용자 확인 없이
패키지를 설치하는 것은 불가능하다. OTL
보안상 문제 때문인거 같은데…

아무튼 그대로 좌절할 수는 없어서 좀더 알아보니 일반 사용자 앱은 불가능하지만
제조사의 key로 싸인된 패키지는 방법이 있는 듯하다.. 하지만 이것도 기기 제조사에서
key를 받기가 어렵기 때문에 불가능 OTL

그러나, 이 방법이 가능한 또다른 경우는 해당 앱이 system app 인 경우이다.
이건 좀 희망이 있다. 비록 루팅을 한다거나, 커스텀 펌웨어를 만든다던가 해야 하지만.=_=

세상에 간단한 일은 없다고 조건이 다 충족되더라도 걍 되는건 아닌데…
자세한건 다음 글을 참조.

간단히 요약하면, 안드로이드에서 패키지를 관리하는 PackageManager 에서 사용자 확인 따위의 과정은
건너띄어 버리고, 실제 설치를 하는 함수를 바로 불러 버리는 거다.

하지만, 이 함수와 관련 인터페이스가 외부에 공개가 되지 않는 관계로, reflection 이라는 좀 고급 기술을 써야한다.
머.. spring 이라던지 이런 류의 프레임워크에서 다 reflection을 사용하여 구현되어 있다지만,
일반 자바 개발자가 흔히 쓸만한 기술은 아니지=_=;

암튼, 다음과 같이 해서 작동하는 건 확인완료.

==

1. Android.Manifest.xml 파일을 열고, android.permission.INSTALL_PACKAGES 퍼미션을 추가한다.
해당 권한은 제조사 key로 싸인된 앱이나 시스템 앱 처럼 시스템 권한을 가진 경우 사용 가능하다.

2. Activity 에 버튼을 하나 추가 하고, 클릭 시에 패키지 설치 코드가 실행되게한다.
직접 코드를 작성해도 되고, 위 글에 있는 소스를 사용해도 된다.

ApplicationManager am = new ApplicationManager(MyActivity.this);
am.installPackage(“/mnt/sdcard/myapp_new.apk”);

3. 버전을 다르게 하여 패키지를 2개 생성한다.

4. 루팅이된 디바이스에 두 개의 패키지 파일을 복사하고, 루트 기능을 사용할 수 있는
파일 관리자 앱을 이용하여 낮은 버전의 패키지를 /system/app 으로 복사한다.

5. 파일 권한 설정 메뉴로 해당 패키지 파일의 권한을 rw-r–r– , 즉 644 로 변경 하고,
user와 group ID는 0으로 설정한다.

6. 디바이스를 재부팅하고, 해당 앱이 어플서랍에 표시되면 시스템 앱 만들기 성공.

7. 아까 만들었던 높은 버전의 패키지 파일은 설치코드에서 지정한 것과 동일한 경로에 동일한
파일명으로 바꾼다.

8. 앱을 실행하고, 버튼을 클릭하면, 잠시 후 앱이 종료된다.

9. 어플리케이션 정보 화면에 들어가서 해당 앱의 버전이 변경되었는지 확인하면 끝!

==

이걸로 한 가지 문제는 해결이 됐는데… 업데이트 후 다시 실행하게 하는게 또 문제군=_=